modularsoftware/laravel-gedcom
Laravel package for reading and parsing GEDCOM genealogy files. It loads GEDCOM data into an object-oriented model of individuals, families, events, and sources, making it easy to work with family-history records inside your Laravel application.
Install and publish
composer require modularsoftware/laravel-gedcom
php artisan vendor:publish --provider="ModularSoftware\LaravelGedcom\Providers\LaravelGedcomServiceProvider"
php artisan migrate
The service provider is auto-discovered on Laravel 8+. Publishing gives you a config file and a set of migrations that create the schema the importer writes into (individuals/persons, families, events, places, sources, citations, notes, media, repositories). Run php artisan migrate before you attempt any import — the parser assumes those tables exist.
Where to look first
config/gedcom.php — DB connection/table options and any import toggles.src/Utils/ — GedcomParser (the import entry point) and the export helpers.src/Models/ — the Eloquent models the importer populates. This is your real API surface; reading them tells you which GEDCOM tags were mapped and which were dropped.src/Facades/Gedcom.php — facade access to the underlying genealogy/gedcom parser if you only want to read a file without persisting.First use case
Import a file and query it back through Eloquent:
use ModularSoftware\LaravelGedcom\Utils\GedcomParser;
$parser = new GedcomParser();
$parser->parse(
storage_path('app/gedcom/family.ged'),
'my-family-tree', // slug / import identifier
['is_media' => false] // import settings
);
$people = \ModularSoftware\LaravelGedcom\Models\Person::where('slug', 'my-family-tree')->get();
Start with a small export (a handful of individuals) from your genealogy software before throwing a 200 MB Ancestry export at it — you want to confirm the schema mapping and encoding handling on something you can verify by eye.
Wrap the import in a queued job. Parsing is CPU- and I/O-heavy and a real GEDCOM export can take minutes. Put GedcomParser::parse() in a ShouldQueue job, log progress, and store the resulting slug so you can correlate the import with the rows it produced. Pass the file path, not the contents, and move uploads to a stable disk first.
Treat the raw file as the source of truth. Persist the uploaded .ged plus a hash, and make imports idempotent by keying on that hash or a versioned slug (family-2024-01-15). Re-importing the same file should either be a no-op or replace the previous import's rows inside a transaction — never silently append duplicates.
Keep a thin service layer over the package. Don't let GedcomParser or the package models leak into controllers and views. A small GedcomImporter / GedcomExporter pair that returns your own DTOs or your own models means upgrading (or forking — see below) the package doesn't ripple through the app.
Import-then-query, rather than parse-per-request. The common production shape is: import once into the DB, then build tree views, pedigree charts, and search entirely off Eloquent relations. If you're parsing GEDCOM on every page load you've mis-architected it.
Export by building from your domain data, not by round-tripping the import tables. If the package exports GEDCOM 5.5.1, feed it your canonical people/families and let it emit HEAD/GEDC VERS 5.5.1/CHAR UTF-8, SUBM, and INDI/FAM records. Validate the output by re-importing it into Gramps or another tool — a self-consistent round-trip through your own importer proves nothing.
Use the parser standalone for read-only features. When you just need to preview a file's contents (record counts, top-level tags, validation) before committing to an import, use the facade/genealogy/gedcom parser directly and skip the database entirely.
Encoding and line endings will bite first. GEDCOM allows ANSEL, ANSI, UTF-8, and UNICODE; many real-world files are Windows-1252 mislabeled or have a BOM, and almost all use CRLF. Normalize to UTF-8 before parsing and strip the BOM, or you'll get mojibake in names and places that's painful to fix after import.
Reference IDs are not your primary keys. GEDCOM cross-references look like @I123@ / @F45@. The importer maps these to internal IDs; if you write custom tag handling, strip the @ and be aware that IDs are only unique within a single file. Never assume they're stable across re-imports.
Dates are approximate by design. Tags like ABT, BEF, AFT, EST, CAL, and ranges BET ... AND ... don't map to a date column. Expect to store a raw string plus parsed components and a "qualifier" field, and handle calendar escapes (@#DJULIAN@) gracefully.
Names are slashed. John /Smith/ Jr. — the surname is delimited by slashes, and there may be multiple NAME tags plus _MARNM. Splitting on spaces will produce garbage.
Line-length rules apply on export. GEDCOM lines cap at 255 characters; long text must be split with CONC (concatenation) and CONT (newline). If the exporter doesn't do this for you, hand-write it or your file will be rejected by stricter tools.
Custom tags are everywhere. Ancestry, MyHeritage, and Gramps all emit _-prefixed tags (_UID, _APID, _MARNM). The package may ignore them silently — check src/Models and the parser's tag handling before assuming data made it through. Media paths in particular are almost always relative to the exporter's machine and won't resolve.
Watch resources on large imports. Do DB::disableQueryLog(), consider chunked inserts, raise memory_limit/set_time_limit, and wrap the whole import in a transaction with periodic commits if it's large. A single 100k-individual file in one transaction can exhaust memory or lock tables for a long time.
Pin the version and be ready to fork. With ~22 stars and a last release in January 2023, this is a small, low-activity package sitting on top of genealogy/gedcom. Read the migration files before running them (they define your schema permanently), pin an exact version in composer.json, and keep your service-layer abstraction thin enough that swapping in a maintained parser is a contained change. The underlying parser's own docs are often more accurate than the wrapper's README — when something behaves unexpectedly, debug against genealogy/gedcom directly.
Debug by dumping parsed records. When a field is empty after import, first dump the raw parsed structure for that individual and confirm the tag existed in the source before blaming the mapping. Nine times out of ten the data was never in the file, or it was in a _-tag the importer skipped.
How can I help you explore Laravel packages today?