Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Webtrees Laravel Package

fisharebest/webtrees

webtrees is an open-source, collaborative web genealogy application built with PHP. It lets families publish and manage GEDCOM-based family trees online, with multi-user editing, privacy controls, charts, reports, and media support.

View on GitHub
Deep Wiki
Context7

Getting Started

First, reset your mental model: fisharebest/webtrees is not a Composer library you require into an existing Laravel app. It is a complete, standalone PHP application that vendors Laravel components (Eloquent, Blade, the container, the router). "Leveraging the package" day-to-day means deploying the app and extending it through its module system — you write code against webtrees' domain classes (Individual, Family, Tree, Fact, Media), not against a documented public API contract.

Minimal bring-up

git clone https://github.com/fisharebest/webtrees.git
cd webtrees
composer install --no-dev --optimize-autoloader

Then:

  1. Create a MySQL/MariaDB database and user.
  2. Create a data directory outside the webroot (GEDCOM files, media, logs live here — it must never be web-accessible).
  3. Point your vhost at the repo root and hit / — the install wizard writes data/config.ini.php with DB credentials and the data path.
  4. Confirm data/config.ini.php is chmod 0644-ish, owned by PHP, and gitignored (it holds credentials).
  5. Log in as admin, create your first tree, and import a GEDCOM 5.5.1 file to get realistic data to develop against.

Where to look first

Path What's there
app/ Fisharebest\Webtrees\ — domain models (Individual, Family, Tree, Fact, Date, Name), services, HTTP layer
modules_v4/ Where your code goes — one directory per module
modules_v2/, modules_v3/ Legacy layout; useful as worked examples but do not target the old API
resources/views/ Blade templates — the canonical examples of how core renders records
resources/lang/ 100+ translation catalogues (.po/.mo)
themes/ Theme modules, if you need branding
tests/ PHPUnit suite plus a WebtreesTestCase base — copy this setup for your own module tests

Your first use case

Do not start with a chart. Start with a ModuleTab that adds a sidebar/research tab to an individual page. It is the smallest end-to-end slice: module registration → route → Blade view → domain model access → privacy check. Once that renders, every other module type (report, block, menu, chart, sidebar, widget) is the same skeleton with a different interface.

namespace MyVendor\ResearchTab;

use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Module\AbstractModule;
use Fisharebest\Webtrees\Module\ModuleTabInterface;
use Psr\Http\Message\ResponseInterface;

class ResearchTab extends AbstractModule implements ModuleTabInterface
{
    public function title(): string
    {
        return I18N::translate('Research notes');
    }

    public function getTabContent(Individual $individual): string
    {
        if (!$individual->canShow()) {
            return '';
        }

        return view($this->name() . '::tab', [
            'individual' => $individual,
            'sources'    => $individual->facts(['SOUR']),
        ]);
    }

    public function isGrayedOut(Individual $individual): bool
    {
        return !$individual->canShow();
    }
}

Drop it in modules_v4/research-tab/, give it a module.php metadata file, then enable it per-tree in Control Panel → Modules. If it does not appear in the list, your class is not autoloadable or the metadata name mismatches the directory.


Implementation Patterns

Pattern 1 — Know which module interface you need

Everything in webtrees is a module implementing one or more interfaces. Pick by where in the UI your output belongs:

  • ModuleTabInterface — extra tab on person/family pages (lowest risk, highest value).
  • ModuleReportInterface — printable reports; usually wraps a Blade view over a FactQuery.
  • ModuleChartInterface — interactive/rendered charts; heaviest to get right.
  • ModuleBlockInterface / ModuleSidebarInterface — dashboard and sidebar widgets.
  • ModuleMenuInterface, ModuleFooterInterface, ModuleAnalyticsInterface — global chrome.
  • ModuleThemeInterface — CSS/branding.
  • ModuleConfigInterface — settings screens, so admins can tune your module without code edits.

A module can implement several; a "research helper" often wants ModuleTabInterface + ModuleConfigInterface (to store which fact tags to surface).

Pattern 2 — Navigate the GEDCOM object graph, don't parse raw GEDCOM

The whole point of the Eloquent layer is that you rarely touch raw GEDCOM text.

$individual->primaryName()->fullName();   // "Name" object, not a string
$individual->sex();                        // 'M' | 'F' | 'U'
$individual->birth()->date();              // Date object (partial dates supported)
$individual->facts(['BIRT', 'DEAT']);      // filter by tag — cheap
$individual->spouses();                    // collection of Individual
$individual->children();                   // from FAMC/FAMS links
$individual->familiesAsSpouse();           // Family models

For bulk work over a big tree, use the query services, not iteration over collections — loading every individual in a 200k-person tree will exhaust memory.

use Fisharebest\Webtrees\Services\IndividualService; // inject via constructor

// Prefer tag-filtered queries + pagination over ->all()
foreach ($tree->individuals() as $i) { /* avoid on large trees */ }

Pattern 3 — Privacy is a rendering concern you must respect

This is the single most common way to ship a security bug in webtrees extensions. Core templates gate everything through the record and fact objects:

if ($individual->canShow()) { ... }
if ($individual->canShowName()) { ... }        // name only, no details
foreach ($individual->facts() as $fact) {
    if ($fact->canShow()) { ... }
}

Never query the database directly to render a person's data — you will bypass the per-tree, per-person, per-user privacy rules and leak living individuals. If you must join across tables, resolve to models and filter with canShow() before output.

For role checks alongside privacy:

use Fisharebest\Webtrees\Auth;

Auth::user();                        // current user or null
Auth::isMember($tree);               // has any access to this tree
Auth::isEditor($tree);               // can edit
Auth::isManager($tree);              // can administer the tree

Pattern 4 — Multi-tree by default

Modules are enabled per tree, and preferences are per tree. Anything you store as configuration should be keyed to the tree, not global:

$tree->getPreference('MY_MODULE_OPTION', 'default');
$tree->setPreference('MY_MODULE_OPTION', $value);

If you find yourself reaching for a global setting, ask whether a family with two trees would want different behaviour — the answer is usually yes.

Pattern 5 — Write through the model layer, keep change history

Collaborative genealogy lives on the change log. Use the model's fact mutation methods so edits land in history and stay GEDCOM-valid:

// update an existing fact rather than string-splicing GEDCOM
$individual->updateFact($fact);      // triggers change recording

Wrap multi-fact edits in a transaction (DB::transaction(...)) so a half-applied merge doesn't corrupt the tree. Never write GEDCOM files directly while the app is running — the DB is the source of truth between imports/exports; export regenerates the file.

Pattern 6 — Blade views and route registration

Views are plain Blade: escape with {{ }}, build forms with CSRF tokens, and reference assets from your module directory.

<form method="post" action="{{ route(MyVendor\ResearchTab::class . ':save') }}">
    @csrf
    <input name="note" value="{{ $note }}">
    <button>{{ I18N::translate('Save') }}</button>
</form>

Route handlers are methods on the module class; the router resolves them by ModuleClass:method. Return $this->viewResponse('module::view', $data) for HTML so you inherit the app's layout and theme, and only fall back to raw Response for non-HTML endpoints (CSV export, JSON for a chart).

Pattern 7 — Internationalise from line one

I18N::translate('Research notes');
I18N::translateContext('tab heading', 'Notes');
I18N::plural('%s source', '%s sources', $count, $count);

Use %s placeholders — never string concatenation — because word order changes across the 100+ supported languages. Add a .po file for your module and the translation tooling picks it up; hardcoded English is a one-way door.

Pattern 8 — Handle dates and names as objects

use Fisharebest\Webtrees\Date;

$date = $individual->birth()->date();
$date->minimumDate();     // Date — for sorting
$date->display();         // localised string
$date->isOK();            // parse succeeded?
$date->qual1;             // ABT / BEF / AFT / EST

Partial and qualified dates are normal in genealogy (ABT 1850, BEF 12 MAR 1900). Never assume a full Y-m-d. Calendars matter too — Julian, Hebrew, and French Republican dates appear in real data.

Pattern 9 — Cache and page anything expensive

use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Cache;

// app-wide key/value store
Registry::cache()->remember('my-module:'.$tree->id(), 3600, fn () => $expensive);

Surname indexes, descendant counts, and report aggregations are the usual suspects. Cache key must include tree id and, if privacy-filtered, the user id — otherwise you serve one user's filtered result to another.

Pattern 10 — Test with the bundled harness

Reuse tests/ conventions: a base test case boots the app container. Seed a tiny GEDCOM fixture with a living person, a deceased person, and a private tree, then assert your module renders nothing for the living person to an anonymous visitor. That one test catches most extension privacy regressions.


Gotchas and Tips

It is an application, not a stable API. app/ internals change between minor releases; there is no semver contract on Fisharebest\Webtrees\* classes. Pin to a specific release tag in your deployment, read the upgrade notes before bumping, and keep every customisation inside modules_v4/. Editing core is a guaranteed upgrade-time rebase.

Version drift is the biggest trap. The package metadata claims a 2016 last release — that reflects the 1.x era, before the Laravel-component rewrite. The modern Eloquent/Blade architecture lives in the 2.x line. Verify which major version your target deployment runs before writing a single line; module directory names (modules_v2/v3/v4) and interfaces differ. If you inherit old tutorials, assume they're for the wrong generation.

The data directory is sacred. GEDCOM files, uploaded media, and logs live outside the webroot. Back it up together with the database — restoring only the DB leaves you with records pointing at missing media. Never place data/ under the document root "temporarily."

Privacy leaks are silent. There is no exception when you forget canShow() — you just render a living person's details to anonymous visitors. Treat every new template as a privacy review: record gate, name gate, per-fact gate. Also remember canShowName() is a distinct, weaker permission used for things like relationship paths.

GEDCOM 5.5.1 has sharp edges. Cross-references look like @I123@/@F45@ pointers; custom tags start with an underscore (_UID, _MILT) and are legal but non-standard; long values are split across 2 CONT lines and must be unescaped before display. Use the provided Gedcom helpers rather than manual explode("\n", $raw).

Date parsing is not strtotime. Date handles ABT, CAL, BEF/AFT, date ranges, and alternate calendars. Calling strtotime() on a webtrees date string will silently produce wrong sort orders and wrong "age at death" calculations.

Large trees change your algorithm. A 100k+ individual tree is normal for this user base. $tree->individuals() materialises everything; use tag-filtered queries, pagination, and Registry::cache(). If a page takes >2s, you almost certainly looped over a whole tree in a view.

Per-tree module enablement confuses first-timers. You can enable a module at the site level and see nothing, because it's disabled for that particular tree. Check both the Control Panel module list and the tree's own module settings before debugging code.

Forms need CSRF and the router needs the class name. Missing @csrf produces a bare 419/403 with no useful message. Route references must use the fully-qualified module class plus :method; a typo gives a 404 that looks like a routing bug but is usually a namespace mismatch.

Cache clears are mandatory after module changes. The module list, translation catalogues, and view compilation cache all persist. When a new tab "doesn't show up," clear the cache before rewriting the module.

Debugging: enable PHP error logging and inspect the logs in the data directory; set display_errors off in production because stray output breaks Blade rendering and GEDCOM downloads. dd()/dump() from Laravel work but will corrupt non-HTML responses — use them only on HTML routes.

Extension points worth knowing beyond tabs: ModuleConfigInterface (admin settings UI — ship this instead of asking users to edit files), the media/Media model for attaching thumbnails to records, and the theme module for CSS overrides. Those three cover most "just one small customisation" requests without forking core.

Licence hygiene. GPL-3.0 means derived modules are generally GPL-compatible too. If you're building for a client, confirm the distribution model before shipping closed-source modules.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
coleus/frame
thtg88/journalism
filagora/filagora-ai-forums
rlacerda83/query-parser
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings