sethphat/eloquent-docs
sethphat/eloquent-docs generates readable documentation for your Eloquent models, drawing details from the models themselves so your Laravel project's data layer stays easy to understand and maintain as it grows.
Minimal path from zero to generated docs:
composer require --dev sethphat/eloquent-docs
php artisan vendor:publish --tag=eloquent-docs # publishes config/eloquent-docs.php
php artisan model:doc # generate @property blocks
Laravel's package auto-discovery registers the service provider, so there is nothing to add to config/app.php. The only file you actually need to touch is config/eloquent-docs.php — that is where you tell the generator which model namespace/path to scan (and optionally which connection to read schema from). Read that file first; it is the whole public API surface.
Your first use case should be a single "fat" model — one with 15+ columns that currently has no @property annotations and shows red squiggles in PhpStorm/VSCode. Run the command, open the model, and confirm the docblock was appended above the class keyword. If that works, expand the config to your whole App\Models path.
Because it is a dev-only concern, install with --dev and treat the generated docblocks as build artifacts you regenerate deliberately, not something you hand-edit.
Schema → docblock regeneration loop. The natural workflow is: write migration → php artisan migrate → php artisan model:doc → commit both the migration and the docblock diff. Keep the two in the same commit so reviewers see the column and its annotation together.
CI drift check. Since the output is deterministic given the DB, you can fail a build when docs are stale:
php artisan migrate --force
php artisan model:doc
git diff --exit-code -- 'app/Models' || (echo "Model docs out of date" && exit 1)
This is the highest-value pattern for teams — it turns "remember to annotate your model" into an enforced invariant.
Static analysis integration. The generated @property / @method blocks are mainly consumed by PHPStan/Larastan and Psalm. Point Larastan at the models and you get type-checked $user->emailed_at->diffForHumans() instead of mixed. This is the real payoff; the IDE autocomplete is a side effect.
Scoped runs during rollout. Don't point it at everything on day one. Add a handful of models to the config, get the diff clean, then expand. Legacy models with dynamic tables, aliased columns, or hand-maintained docblocks are the ones that produce noisy or wrong output.
Composer script chaining. Wire it into a composer docs script alongside Pint so formatting is normalised after generation:
"scripts": {
"docs": ["@php artisan model:doc", "vendor/bin/pint app/Models"]
}
Multi-connection apps. If models live on different databases, the connection has to be resolvable at generation time — the generator reads real schema, not migration files, so the connection must be reachable from wherever you run the command.
$casts are not fully reflected. Columns stored as string but cast to datetime, array, AsEnumCollection, etc. will get the raw DB type. Expect to correct a few annotations by hand, or accept looser types.$appends, and relationships are generally out of scope. You will still hand-write @property-read for relations and computed attributes. Treat the generator as covering the base table only.$table conditionally (e.g. sharded/tenant tables) or resolve connections at runtime will produce misleading annotations — exclude them from the scan list.app/Models tree.php artisan config:clear after editing config/eloquent-docs.php — a cached config silently ignores your new model paths and you will chase a phantom bug.require-dev, and re-verify the command still runs after each Laravel major upgrade — an abandoned dev tool that silently no-ops is worse than none.@property-read for relationships), the pragmatic answer is a small post-processing script over the generated files rather than forking the package.Bottom line: worth adopting as a dev-only generator in a CI drift check, provided you treat the database schema — not the docblock — as the source of truth and accept that casts and relations remain manual work.
How can I help you explore Laravel packages today?