nickwelsh / eloquent-zero
Generate Zero schemas from Eloquent Models
Fund package maintenance!
Requires
- php: ^8.4
- illuminate/contracts: ^11.0||^12.0||^13.0
- spatie/laravel-package-tools: ^1.16
Requires (Dev)
- larastan/larastan: ^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.8
- orchestra/testbench: ^9.0.0||^10.0.0||^11.0.0
- pestphp/pest: ^4.0
- pestphp/pest-plugin-arch: ^4.0
- pestphp/pest-plugin-laravel: ^4.0
- phpstan/extension-installer: ^1.4
- phpstan/phpstan-deprecation-rules: ^2.0
- phpstan/phpstan-phpunit: ^2.0
This package is auto-updated.
Last update: 2026-04-26 01:53:06 UTC
README
eloquent-zero generates a typed Zero schema from your Laravel Eloquent models and Postgres database.
It reads your models, database columns, primary keys, enum types, and Eloquent relationships, then writes a schema.ts file ready for Zero. It can also sync a Postgres publication for those models.
Warning This package is very early. Expect rough edges, missing features, and breaking changes.
What it does
- generates Zero tables from Eloquent models
- maps
belongsTo,hasOne,hasMany, andbelongsToManyrelationships - respects model hidden fields and column allowlists
- syncs Postgres publication column lists from model metadata
- supports renaming the emitted Zero schema with a PHP attribute
- validates that migrations are current before generating
Requirements
- PHP 8.4+
- Laravel 11+
- PostgreSQL
eloquent-zero currently only supports Postgres connections.
Installation
You can install the package via composer:
composer require nickwelsh/eloquent-zero:dev-main
You can publish and run the migrations with:
php artisan vendor:publish --tag="eloquent-zero-migrations"
php artisan migrate
You can publish the config file with:
php artisan vendor:publish --tag="eloquent-zero-config"
Published config:
<?php use NickWelsh\EloquentZero\Support\Casing; use NickWelsh\EloquentZero\Support\Mode; return [ 'mode' => Mode::OptOut, 'model_search_directories' => [ app_path('Models'), ], 'models' => [], 'tables' => [], 'output_path' => resource_path('js/zero/schema.ts'), 'table_name_casing' => Casing::Camel, 'column_name_casing' => Casing::Camel, 'use_wayfinder' => false, 'connection' => null, 'allow_multiple_connections' => false, 'publication_name' => null, ];
Usage
Generate the schema:
php artisan generate:zero-schema
Generate from explicit models only:
php artisan generate:zero-schema \ --model="App\\Models\\User" \ --model="App\\Models\\Post"
Override output path:
php artisan generate:zero-schema --path=resources/js/zero/custom-schema.ts
Force a connection:
php artisan generate:zero-schema --connection=pgsql
Sync Postgres publication:
php artisan zero:sync-publication
Validate publication changes without applying:
php artisan zero:sync-publication --dry-run
Example
Given these models:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; class User extends Model { public function posts(): HasMany { return $this->hasMany(Post::class); } } class Post extends Model { public function user(): BelongsTo { return $this->belongsTo(User::class); } }
eloquent-zero will generate a schema shaped like:
const user = table('users').columns({ id: string(), }).primaryKey('id') const post = table('posts').columns({ id: string(), userId: string().from('user_id'), }).primaryKey('id')
with matching relationships(...) blocks and exported Zero types.
Attributes
#[ZeroName('...')]
Rename the emitted Zero schema while still reading from the model's underlying table.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use NickWelsh\EloquentZero\Attributes\ZeroName; #[ZeroName('people')] class User extends Model {}
That generates:
const person = table('people') .from('users')
#[ZeroColumns([...])]
Limit which columns are included in the generated schema.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use NickWelsh\EloquentZero\Attributes\ZeroColumns; #[ZeroColumns(['id', 'title'])] class Comment extends Model {}
If a relation needs a foreign key column that you excluded, eloquent-zero will force it back in and emit a warning.
#[ZeroColumns] only affects emitted TypeScript schema. It does not change Postgres publication columns.
#[ZeroExclude([...])]
Exclude columns from Zero Postgres publication sync.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use NickWelsh\EloquentZero\Attributes\ZeroExclude; #[ZeroExclude(['password'])] class User extends Model {}
Publication rules:
- default: infer excluded publication columns from model
$hidden - override: if
#[ZeroExclude([...])]exists, use only that list for publication exclusion - safety: required relation columns are forced back into publication with warning
#[ZeroIgnore]
Skip a model when running in opt-out mode.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use NickWelsh\EloquentZero\Attributes\ZeroIgnore; #[ZeroIgnore] class InternalAuditLog extends Model {}
#[ZeroGenerate]
Only include marked models when running in opt-in mode.
<?php namespace App\Models; use Illuminate\Database\Eloquent\Model; use NickWelsh\EloquentZero\Attributes\ZeroGenerate; #[ZeroGenerate] class User extends Model {}
Model selection
mode controls how models are picked:
Mode::OptOut: include discovered models unless they have#[ZeroIgnore]Mode::OptIn: include only models with#[ZeroGenerate]
Models can come from:
model_search_directories- explicit
modelsconfig entries --modelCLI arguments
model_search_directories accepts plain directories and glob patterns, for example base_path('modules/*/Models').
Tables without Eloquent models can come from tables config:
'tables' => [ 'model_has_roles' => true, 'model_has_permissions' => ['permission_id', 'model_type', 'model_id'], ],
Use true for all columns. Use an array to allow only listed columns. These tables are added to generated Zero schema and Postgres publication sync.
Name casing
By default, table and column names are emitted in camelCase.
Examples:
- table
blog_posts-> Zero schemablogPosts - column
created_at-> Zero columncreatedAt
You can change this with:
table_name_casingcolumn_name_casing
Safety checks
Before generation, the package:
- verifies pending migrations do not exist
- verifies relations match real database foreign keys
- verifies
ZeroColumnsentries point at real columns - falls back to a single unique index if a table has no primary key
Testing
vendor/bin/pest
Changelog
Please see CHANGELOG for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security Vulnerabilities
Please review our security policy on how to report security vulnerabilities.
Credits
License
The MIT License (MIT). Please see License File for more information.