Search by

zero-to-prod / db-model

zero-to-prod

Generates PHP Artifacts to Represent Database Schemas

Package info

github.com/zero-to-prod/db-model

pkg:composer/zero-to-prod/db-model

Statistics

Installs: 564

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.1 2026-09-03 20:10 UTC

This package is auto-updated.

Last update: 2026-09-03 20:11:23 UTC


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

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 — what db-model:install does, without a prompt to answer. Every argument defaults to the current setting, databases to the connection's own database. An unknown database name is answered with the list of real ones, and a config/db-model.php that says something else is left alone and reported until the call passes overwrite: 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.