zero-to-prod / db-model
Generates PHP Artifacts to Represent Database Schemas
Requires
- php: ^8.5
- illuminate/console: ^13.0
- illuminate/database: ^13.0
- illuminate/support: ^13.0
Requires (Dev)
- laravel/mcp: ^0.9
- laravel/pint: ^1.30
- orchestra/testbench: ^11.0
- pestphp/pest: ^5.0
- pestphp/pest-plugin-rector: ^5.0
- phpstan/phpstan: ^2.2
- rector/rector: ^2.5
Suggests
- laravel/mcp: Required by the MCP server that exposes this package's documentation to coding agents.
Provides
None
Conflicts
None
Replaces
None
README
Generates PHP artifacts to represent database schemas.
This is useful if you want a single source of truth for your database schema that you can reference in your codebase.
Install the package with db-model:install.
Scaffold PHP representation of your database tables with db-model:generate.
Check them with db-model:check.
Requirements
- PHP
^8.5 - Laravel 13
- MySQL 8
Installation
composer require zero-to-prod/db-model
Configuration
CLI install:
php artisan db-model:install
Rerunning it is safe.
To publish the configuration file by itself instead:
php artisan vendor:publish --tag=db-model-config
namespace and path must describe the same place: a directory holding one
subdirectory per schema, named after it.
'namespace' => 'App\\Sources\\Db', 'path' => app_path('Sources/Db'),
Usage
Scaffold a table enum beside it for every table in the database:
php artisan db-model:generate # writes to the configured path php artisan db-model:generate --dry-run # reports what it would write
// app/Sources/Db/App/Users.php namespace App\Sources\Db\App; use ZeroToProd\DbModel\Column; use ZeroToProd\DbModel\ColumnType; use ZeroToProd\DbModel\HasColumnAttribute; use ZeroToProd\DbModel\Table; /** * @method string type() * @method string|null comment() * @method int|null length() * @method bool|null nullable() * @method bool|null unique() * @method bool|null primary_key() * @method bool|null auto_increment() */ #[Table( schema: App::class, attributes: [ Table::name => 'users', Table::comment => 'The users table', Table::collate => 'utf8mb4_unicode_ci', ])] enum Users: string { use HasColumnAttribute; #[Column([ Column::name => self::email, Column::comment => 'The users email', Column::type => ColumnType::varchar->value, Column::length => 255, Column::nullable => false, Column::unique => true, ])] case email = 'email'; }
Users::email->value; // 'email' Users::email->rules(); // ['required', 'string', 'max:255'] Users::email->type(); // 'varchar' Users::email->length(); // 255 Users::email->unique(); // true Users::email->comment(); // 'The users email'
rules() states only what the schema itself constrains. Uniqueness,
confirmation and any format the database does not enforce belong to the
request:
public function rules(): array { return [ 'email' => [...Users::email->rules(), 'email', 'unique:users'], 'password' => [...Users::password->rules(), 'confirmed'], ]; }
// config/db-model.php 'trait' => App\Sources\Db\HasColumn::class,
php artisan db-model:generate # the enums now `use HasColumn`
Users::created_at->cast(); // 'immutable_datetime' Users::created_at->rules(); // ['nullable', 'date'] — still from the package
Interfaces
implements names an interface every generated table enum declares — a single
class-string, or a list of them. It defaults to null, which declares none:
// config/db-model.php 'implements' => App\Sources\Db\Authority::class, 'implements' => [App\Sources\Db\Authority::class, App\Sources\Db\Sortable::class],
php artisan db-model:generate
enum Users: string implements Authority, Sortable { use HasColumn;
The generator writes the clause and the imports it needs. Satisfying the
interface is yours to do, on the trait above.
Checking for drift
Add the check to your pipeline. It names every difference and exits non-zero:
php artisan db-model:check
Both commands take --schema to select the schema, defaulting to App, plus
--connection and --database to say where to read it from. Without them the
default connection and its own database are read:
php artisan db-model:check --schema=Reporting --connection=mysql --database=reporting php artisan db-model:generate --schema=Reporting --database=reporting --path=/tmp/reporting
Agent development
The package registers an MCP server so
coding agents can read how it is meant to be used. It requires
laravel/mcp, and registers nothing without
it.
composer require --dev laravel/mcp php artisan mcp:start db-model
Register it with your agent:
claude mcp add db-model -- php artisan mcp:start db-model
Three tools are exposed:
readme— this document.api— the exact signature of every public class, property and method. Anything unlisted is internal and may change in any release.install— whatdb-model:installdoes, without a prompt to answer. Every argument defaults to the current setting,databasesto the connection's own database. An unknown database name is answered with the list of real ones, and aconfig/db-model.phpthat says something else is left alone and reported until the call passesoverwrite: true.
{
"namespace": "App\\Sources\\Db",
"path": "app/Sources/Db",
"implements": ["App\\Sources\\Db\\Authority"],
"databases": ["app", "reporting"],
"generate": true
}
Point the handle somewhere else, or turn the server off, in
config/db-model.php:
'mcp' => [ 'enabled' => true, 'handle' => 'db-model', ],
Development
composer check # lint, rector, phpstan, 100% coverage, bc-check — mutates nothing composer fix # rector then pint composer mcp list # the server's tools composer mcp call api '{}' # call one
composer check requires a coverage driver (Xdebug or pcov); without one Pest
cannot satisfy the --min=100 gate.
The tests read a real MySQL schema. They create the database named by
DB_DATABASE if it does not exist, and drop every table in it. Point them at a
throwaway database — phpunit.xml defaults to testing_db_model on
127.0.0.1:3306:
DB_HOST=127.0.0.1 DB_PORT=3306 DB_DATABASE=testing_db_model \
DB_USERNAME=sail DB_PASSWORD=password composer test
License
MIT. See LICENSE.