nickwelsh/eloquent-zero

Generate Zero schemas from Eloquent Models

Maintainers

Package info

github.com/nickwelsh/eloquent-zero

pkg:composer/nickwelsh/eloquent-zero

Fund package maintenance!

Nick Welsh

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-04-26 01:52 UTC

This package is auto-updated.

Last update: 2026-04-26 01:53:06 UTC


README

Latest Version on Packagist GitHub Tests Action Status GitHub Code Style Action Status Total Downloads

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, and belongsToMany relationships
  • 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 models config entries
  • --model CLI 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 schema blogPosts
  • column created_at -> Zero column createdAt

You can change this with:

  • table_name_casing
  • column_name_casing

Safety checks

Before generation, the package:

  • verifies pending migrations do not exist
  • verifies relations match real database foreign keys
  • verifies ZeroColumns entries 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.