A generator plugin that translates your Prisma schema into Laravel‑ready
- Database Migrations
- Eloquent Models & PHP Enums
- TypeScript model & enum types
Built in strict TypeScript with fully‑customisable stubs, grouping, smart merge updates, and an optional TS types generator.
npm install prisma-laravel-migrate --save-dev
# requires the Prisma CLI in your projectThe generators read options in three tiers (highest → lowest):
- Environment override –
PRISMA_LARAVEL_CFG=/path/to/laravel.config.js - Shared project file –
prisma/laravel.config.js(auto‑loaded if present) generator … { … }blocks inschema.prisma
A key in tier ① shadows the same key in ② and ③; tier ② shadows tier ③.
/** Global to all Prisma‑Laravel generators */
module.exports = {
/* -- table decoration ------------------------------- */
tablePrefix: "tx_",
tableSuffix: "_arch",
/* -- default stub root ------------------------------ */
stubDir: "prisma/stubs",
/* -- global dry‑run --------------------------------- */
noEmit: false,
/* -- override default outputs ----------------------- */
output: {
migrations: "database/migrations",
models: "app/Models",
enums: "app/Enums",
},
/* -- per‑generator overrides ------------------------ */
migrate: {
groups: "./prisma/migrate-groups.js",
rules : "./prisma/custom-rules.js",
},
modeler: {
groups: [
{ stubFile: "audit.stub", tables: ["logs","audit_trails"] },
],
outputEnumDir: "app/Enums",
overwriteExisting: true,
allowedPivotExtraFields: ["scope"],
// use awobaz/compoships for multi-column relations
awobaz: true,
},
/**
* NEW: TypeScript generator (models + enums → TS types)
*/
types: {
// Where generated TS goes (default: "resources/ts/prisma")
outputDir: "resources/ts/prisma",
// Emit .d.ts instead of .ts when true
declaration: false,
// "interface" | "type" for model declarations
shape: "interface",
// Optional module wrapper for the main types file
// declare module "database/prisma" { ... }
moduleName: "database/prisma",
// Example scalar mapping overrides
scalarMap: {
// BigInt: "bigint",
// Decimal: "string",
// Json: "unknown",
},
// If true: nullable fields become optional properties
nullableAsOptional: false,
// If true: lists become ReadonlyArray<T>
readonlyArrays: false,
// Name decoration for interfaces/types
namePrefix: "",
nameSuffix: "",
},
};There is also a small section in the codebase labelled “Related Generator Options (Formatting & Compoships)” covering:
prettier(per‑generator): enables formatting via Prettier for PHP/TS outputs during merge.modeler.awobaz: whether to emit Compoships‑aware relationships (requiresawobaz/composhipsin your Laravel app).
Type reference
export interface Rule {
test(
def: ColumnDefinition,
allDefs: ColumnDefinition[],
dmmf: DMMF.Document,
): boolean;
render(
def: ColumnDefinition,
allDefs: ColumnDefinition[],
dmmf: DMMF.Document,
): Render;
}
/* ------------------------------------------------------------
* Re-usable stub-group description
* ---------------------------------------------------------- */
export interface StubGroupConfig extends FlexibleStubGroup {
/** Path relative to stubDir/<type>/ (e.g. "auth.stub") */
stubFile: string;
tables: string[]; // ["users","accounts",…] or enum names
}
/**
* Back-compat + new matching options.
* Supply EITHER `tables` *or* (`include` / `exclude` / `pattern`).
*/
interface FlexibleStubGroup {
/** Path relative to stubDir/<type>/, e.g. "auth.stub" */
stubFile: string;
/** Old style - explicit white-list */
tables?: string[];
/** New style – include list ( '*' means “all tables” ) */
include?: string[] | '*';
/** New style – blacklist applied after include / pattern */
exclude?: string[];
/** New style – RegExp OR minimatch glob(s) */
pattern?: RegExp | string | Array<RegExp | string>;
}
/* ------------------------------------------------------------
* Per-generator overrides (migration / modeler)
* ---------------------------------------------------------- */
export interface LaravelGeneratorConfig {
tablePrefix?: string;
tableSuffix?: string;
/** Override stubDir only for this generator */
stubDir?: string;
/** Where the generated PHP goes (overrides block) */
outputDir?: string;
overwriteExisting?: boolean;
/** Allow formatting with prettier */
prettier?: boolean;
/**
* Stub grouping:
* • string – path to a JS module exporting StubGroupConfig[]
* • array – the group definitions themselves
*/
groups?: string | StubGroupConfig[];
/** Skip file emission for *this* generator only */
noEmit?: boolean;
/** Default namespace for local imports */
namespace?: "App\\";
}
/* ------------------------------------------------------------
* Top-level shared config (visible to all generators)
* ---------------------------------------------------------- */
export interface LaravelSharedConfig {
/** Table name decoration */
tablePrefix?: string;
tableSuffix?: string;
/** Default stub root (migration/, model/, enum/, ts/) */
stubDir?: string;
/** Global “don’t write files” switch */
noEmit?: boolean;
/** Override default output folders (PHP side) */
output?: {
migrations?: string;
models?: string;
enums?: string;
};
/** Per-generator fine-tuning */
migrate?: Partial<MigratorConfigOverride>;
modeler?: Partial<ModelConfigOverride>;
/** NEW: TypeScript generator overrides */
types?: Partial<TypesConfigOverride>;
}
/* --- Migrator-specific extra keys ---------------------------------------- */
export interface MigratorConfigOverride extends LaravelGeneratorConfig {
/**
* Custom migration rules:
* • string – path to JS module exporting Rule[]
* • Rule[] – rules array inline
*/
rules?: string | Rule[];
stubPath?: string;
defaultMaps?: DefaultMaps;
}
export interface ModelConfigOverride extends LaravelGeneratorConfig {
modelStubPath?: string;
enumStubPath?: string;
/** Extra folder for enums (modeler only) */
outputEnumDir?: string;
/** use awobaz/compoships */
awobaz?: boolean;
/** Extra fields allowed on pivot models */
allowedPivotExtraFields?: string[];
}
/* ------------------------------------------------------------
* TypeScript generator overrides
* ---------------------------------------------------------- */
export interface TypesConfigOverride extends LaravelGeneratorConfig {
/** Where generated TS types should be written, e.g. "resources/ts/prisma" */
outputDir?: string;
/** Emit `.d.ts` declaration files instead of `.ts` source files. */
declaration?: boolean; // default: false → .ts
/** Use `interface` or `type` for model declarations. */
shape?: "interface" | "type"; // default: "interface"
/**
* Map Prisma scalar types → TypeScript types.
* Keys are Prisma scalar names ("Int", "BigInt", "Decimal", "Json", "DateTime", etc.).
*/
scalarMap?: Record<string, string>;
/** If true, nullable fields become optional: `foo?: string` instead of `foo: string | null`. */
nullableAsOptional?: boolean;
/** If true, lists are emitted as `ReadonlyArray<T>` instead of `T[]`. */
readonlyArrays?: boolean;
/** Optional name decoration for generated types/interfaces. */
namePrefix?: string;
nameSuffix?: string;
/**
* Optional root module/namespace hint.
* When set, the main types file is wrapped in:
* declare module "…" { ... }
*/
moduleName?: string;
/** Where models should import enums from (default: "./enums"). */
enumImportFrom?: string;
}Even with the shared file you may still keep minimal blocks in schema.prisma:
generator migrate {
provider = "prisma-laravel-migrations"
stubDir = "./prisma/stubs"
}
generator modeler {
provider = "prisma-laravel-models"
stubDir = "./prisma/stubs"
}
// NEW: TypeScript types generator
generator types {
provider = "prisma-laravel-types"
stubDir = "./prisma/stubs"
}| Key | Notes | |
|---|---|---|
outputDir / output |
Destination folder (outputDir overrides output) — PHP and TS. |
|
outputEnumDir |
(modeler) directory for generated PHP enum classes. | |
stubDir |
Root stub folder (migration/, model/, enum/, ts/). |
|
tablePrefix |
String prepended to every generated physical table name. | |
tableSuffix |
String appended to every generated physical table name. | |
groups |
JS module or inline array that maps stub files to table/model/enum/TS groups. | |
noEmit |
If true, generator parses and validates but does not write any files (dry-run / CI mode). |
|
prettier |
Per-generator toggle for formatting (PHP or TS) before 3‑way merge. | |
awobaz |
(modeler) Emit Compoships‑aware relationships; requires awobaz/compoships. |
|
scalarMap |
(types) Map Prisma scalars → TS type names. | |
nullableAsOptional |
(types) Emit foo?: T for nullable fields instead of `foo: T |
null`. |
readonlyArrays |
(types) Emit ReadonlyArray<T> instead of T[]. |
|
moduleName |
(types) Wrap main types output in declare module "…" {}. |
A group links one stub file to many tables (or enums / TS variants).
From v2 you can keep the old tables: [...] list or use the new, more
expressive selectors:
| Key | Type / Example | Meaning |
|---|---|---|
tables |
["users","accounts"] |
Classic explicit list – exact names only |
include |
["audit_*","logs"] or "*" |
White‑list using globs or '*' (all) |
exclude |
["*_archive","failed_jobs"] |
Black‑list applied after include / pattern |
pattern |
/^temp_/ or "report_*" |
RegExp or glob(s) – match if any hits |
Only one of
tablesor the new selector trio is required.
generator migrate {
provider = "prisma-laravel-migrations"
stubDir = "./prisma/stubs"
groups = "./prisma/group-stubs.js"
}/** @type {import('prisma-laravel-migrate').FlexibleStubGroup[]} */
module.exports = [
// 1. legacy explicit list
{
stubFile: "auth.stub", // stubs/migration/auth.stub
tables: ["users","accounts","password_resets"],
},
// 2. regex + blacklist
{
stubFile: "audit.stub",
pattern : /^audit_/,
exclude : ["audit_archive"],
},
// 3. catch‑all
{
stubFile: "catch-all.stub",
include : "*",
exclude : ["failed_jobs","migrations"],
},
];- Table‑specific —
stubs/<type>/<table>.stub - First matching group (objects are checked top‑to‑bottom)
- Default —
stubs/<type>/index.stub
If a group’s
stubFiledoes not exist it is skipped, so you may leave unused groups in place without breaking the build.
prisma/stubs/
├── migration/index.stub
├── model/index.stub
├── model/simple-model.stub
├── enum/index.stub
└── ts/index.stub
Add table‑specific overrides at
stubs/<type>/<table>.stub (e.g. stubs/model/users.stub, stubs/ts/Account.stub).
migration/andmodel/stubs are PHP template literals.enum/stubs are PHP enum templates.ts/stubs are JavaScript template literals evaluated by the TS printer.
| Command | Purpose |
|---|---|
init |
Inject generator blocks & scaffold stubs |
customize |
Create per-table stub overrides |
gen |
Run prisma generate then Laravel + TS generators |
npx prisma-laravel-cli init --schema=prisma/schema.prismanpx prisma-laravel-cli customize \
-t migration,model,ts \
-n users,accounts \
--force| Flag | Description |
|---|---|
-t, --types |
Required. Stub types (migration, model, enum, ts). enum may not mix with others. |
-n, --names |
Required. Table or enum names (users,accounts). |
--force |
Overwrite existing stub files. |
--config |
Alternate CLI config path. |
npx prisma-laravel-cli gen --config=prisma/laravel.config.js
# skip prisma generate step
npx prisma-laravel-cli gen --config=prisma/laravel.config.js --skipGenerateprisma/laravel.config.js
module.exports = {
migrate: {
outputDir: "database/migrations",
stubDir: "prisma/stubs",
groups: "./prisma/group-stubs.js",
},
modeler: {
outputDir: "app/Models",
outputEnumDir: "app/Enums",
stubDir: "prisma/stubs",
groups: "./prisma/group-stubs.js",
},
types: {
outputDir: "resources/ts/prisma",
stubDir: "prisma/stubs",
moduleName: "database/prisma",
},
};-
Generator builds a full new file from your schema & stubs.
-
Performs a git‑style 3‑way merge (using
node-diff3):- base = last generator output (
.prisma-laravel/backups/...) - ours = file on disk (user edits)
- theirs = freshly generated file
- base = last generator output (
-
Non‑conflicting changes merge automatically; conflicts are wrapped with
<<<<<<<,=======,>>>>>>>. -
New imports are merged, duplicates skipped.
-
Baseline copy is updated in the backups folder.
The same merge pipeline is used for:
- PHP migrations (
type: "migrator"in the diff writer), - PHP models/enums (
type: "model"), - TypeScript outputs (
type: "ts", using thetypescriptPrettier parser).
Delete the marker block and set noEmit = true to stop updates for a file.
Stubs are JavaScript template literals. Escape ` and ${ } if you want them literally.
Fully custom model stubs If you remove the
${content}placeholder and the marker block, the generator leaves the file untouched. Keep the markers if you want automated updates but customised surroundings.
Enum index.stub (PHP)
<?php
namespace App\Enums;
enum ${enumDef.name}: string
{
${enumDef.values.map(v => ` case ${v} = '${v}';`).join('\n')}
}Migration index.stub
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('${tableName}', function (Blueprint $table) {
${columns}
});
}
public function down(): void
{
Schema::dropIfExists('${tableName}');
}
};TS ts/index.stub (module-level)
${imports}
${body}imports→ merged import block for all non-stubbed models (including enums).body→ fulldeclare module "…" { … }wrapper generated by the TS printer.content(exposed to the stub function) is just the inner interfaces/types without thedeclare modulewrapper, if you need it for advanced patterns.
Per-model TS stubs under stubs/ts/<Model>.stub receive:
(model, imports, content, body, moduleName) => stringand are free to ignore or wrap the default body however they like.
Expand stub
<?php
namespace App\Models;
${model.imports}
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\{ BelongsTo, HasMany, BelongsToMany };
class ${model.className} extends Model
{
protected $table = '${model.tableName}';
/* Mass Assignment */
protected $fillable = [
${model.properties.filter(p => p.fillable).map(p => ` '${p.name}',`).join('\n')}
];
protected $guarded = [
${(model.guarded ?? []).map(n => ` '${n}',`).join('\n')}
];
/* Hidden & Casts */
protected $hidden = [
${model.properties.filter(p => p.hidden).map(p => ` '${p.name}',`).join('\n')}
];
protected $casts = [
${model.properties.filter(p => p.cast).map(p => ` '${p.name}' => '${p.cast}',`).join('\n')}
${model.properties.filter(p => p.enumRef).map(p => ` '${p.name}' => ${p.enumRef}::class,`).join('\n')}
];
/* Interfaces metadata */
public array $interfaces = [
${Object.entries(model.interfaces).map(([k,i]) =>
` '${k}' => {${i.import ? ` import: '${i.import}',` : ''} type: '${i.type}' },`).join('\n')}
];
// This structure is useful for packages like fumeapp/modeltyper which
// read interface metadata to build TypeScript helpers.
/* Relationships */
${model.relations.map(r => {
const args = [r.modelClass, r.foreignKey ? `'${r.foreignKey}'` : '', r.localKey ? `'${r.localKey}'` : ''].filter(Boolean).join(', ');
return ` public function ${r.name}(): ${r.type.charAt(0).toUpperCase()+r.type.slice(1)}\n {\n return $this->${r.type}(${args});\n }`;
}).join('\n\n')}
// Or
${relationships}
${content}
}protected $casts = [
'status' => StatusEnum::class,
];Point the generator’s rules field to a JS file exporting an array of
objects that implement the Rule interface:
generator migrate {
provider = "prisma-laravel-migrations"
stubDir = "./prisma/stubs"
rules = "./prisma/custom-rules.js"
}prisma/custom-rules.js
/** @type {import('prisma-laravel-migrate').Rule[]} */
module.exports = [
{
// Always add an `archived` boolean column defaulting to false
test(def) {
return def.name === "archived" && def.migrationType === "boolean";
},
render() {
return {
column: "archived",
snippet: ["$table->boolean('archived')->default(false);"],
};
},
},
// add more Rule objects...
];Rule execution order
- Built‑in rules
- Custom rules (executed in array order)
ColumnDefinition extends Prisma’s DMMF.Field, so all raw Prisma
properties remain accessible.
import { DMMF } from "@prisma/generator-helper";
export interface ColumnDefinition extends DMMF.Field {
migrationType: MigrationType; // e.g. "unsignedBigInteger"
args?: string[];
nullable?: boolean;
unsigned?: boolean;
hasDefaultValue: boolean;
default?: string | number | boolean | null;
relationship?: {
on: string;
references?: string[];
fields: string[];
onDelete?: string;
onUpdate?: string;
};
ignore?: boolean;
}Common checks inside a rule:
def.kind // "scalar" | "enum" | "object"
def.type // original Prisma scalar
def.migrationType // mapped Laravel builder name
def.isId📝 Prisma DMMF docs: https://github.com/prisma/prisma/blob/main/packages/prisma-schema-wasm/src/tests/snapshot/dmmf.md
These inline comment directives let you shape what the generators emit from your Prisma schema — without changing runtime schema semantics.
You can attach them either:
- Per–field (inline or
///directly above the field), or - Per–model/enum (any
///inside the model/enum’s block).
This document focuses on the new & updated directives and how they interact with existing ones like
@fillable,@hidden,@guarded,@cast{…},@type{…},@with,@trait:…,@extend:…,@implements:…,@observer:…,@factory:…,@touch{…},@appends{…}.
The same directives are visible to the PHP generators (migrator + modeler) and to the TS types generator, which uses them to:
- apply
@type{…}overrides for TS field types/imports, - honour
@withwhen deciding whether relation properties are optional, and - strip directive noise from generated JSDoc/docblocks.
@local— replaces the prior@ignoredirective. Skips generating a single relation method on a model. Supports scoped arguments.@silent— marks an entire model or enum to be parsed but not emitted (no model file, no migration, no enum class). Supports scoped arguments.@morph(...)— declares owner-side polymorphic relations (morphOne,morphMany,morphToMany,morphedByMany) with optional chained calls. Child-sidemorphToremains auto-detected from scalar pairs likecommentable_id+commentable_type.
| Directive | Scope | Purpose |
|---|---|---|
@fillable |
Field or @fillable{…} on model |
Adds field(s) to $fillable. |
@hidden |
Field or @hidden{…} on model |
Adds field(s) to $hidden. |
@guarded |
Field or @guarded{…} on model |
Adds field(s) to $guarded. |
@cast{…} |
Field | Adds a cast entry to $casts. |
@type{ import:'…', type:'…' } |
Field | Exposes a PHP/interface/TS type hint for downstream tooling. |
@with / @with(a,b,…) |
Field / Model | Marks relations to eager-load via $with and as non‑optional in TS types. |
@trait:… @extend:… @implements:… @observer:… @factory:… @touch{…} @appends{…} |
Model | Class customization & extras (traits, parents, observers, factories, touches, appends). |
@local |
Relation Field | Skip generating that specific relation method on the model. Replaces @ignore. |
@silent |
Model / Enum | Do not emit files for this entity (model + migration / enum). |
@morph(…) |
Model | Declare owner-side polymorphic relations; child-side morphTo is auto-detected. |
@pivot / @pivot(a,b,…) |
Pivot model and/or scalar fields | Explicitly mark extra pivot columns to include in generated withPivot(…) chains. |
@withTimestamps |
Pivot model / relation field | Instructs the generator to append ->withTimestamps() on the relation definition. |
@pivotAlias(name) |
Pivot model | Sets the pivot attribute alias; generator should add ->as('name') on the relation. |
Syntax options • Inline:
balance Decimal /// @fillable @cast{decimal:2}• Block above field:/// @hidden• Model list:/// @fillable{name,balance}• Model eager‑load:/// @with(posts,roles)• Traits / implements:/// @trait:Illuminate\Auth\Authenticatable/// @implements:Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract• Observers / factory:/// @observer:App\Observers\UserObserver/// @factory:UserFactory• Touches / appends:/// @touch{company,profile}/// @appends{full_name,age}
Note: Directives like
@fillable,@hidden,@guarded,@with,@touch, and@appendssupport all of the following syntaxes:
/// @fillable{name,balance}/// @fillable(name,balance)/// @fillable: name,balance
Use @local on a relation field to prevent generating that one PHP relation method in the model class.
Forms:
@local→ both Model + Migrator@local(model)→ Model only@local(migrator)or@local(migration)→ Migrator only@local(both)/@local(all)/@local(*)→ Both@local(model,migrator)→ Both
model Account {
id Int @id @default(autoincrement())
user User? @relation(fields: [userId], references: [id]) /// @local
userId Int?
}Effect: the user() method is not written to Account.php. If scope includes Migrator, the migration also skips generating the FK/constraint. Other methods remain unaffected. TS types still include the user?: User | null navigation property unless you override it with @type.
Apply /// @silent inside a model or enum docblock to mark it as non-emitting.
Forms:
@silent→ both Model + Migrator@silent(model)→ suppress only the model file@silent(migrator)/@silent(migration)→ suppress only the migration@silent(both)/@silent(all)/@silent(*)→ suppress both@silent(model,migrator)→ suppress both
/// @silent
model AuditTrail {
id Int @id @default(autoincrement())
note String
}Effect: no Eloquent model and/or no migration file are emitted for AuditTrail depending on the scope. For enums, no PHP enum is emitted. The TS generator can still expose types for AuditTrail if you wish (or you can add an additional TS-side filter later).
If a model contains scalar columns named <base>_id and <base>_type — for example commentable_id and commentable_type — the generator emits the child-side method automatically:
public function commentable()
{
return $this->morphTo('commentable');
}No directive is required for morphTo.
On the TS side, the generator also detects these pairs and adds a corresponding appended property like:
commentable?: any;which you can refine via @appends{commentable: Comment} or project‑specific
TS overrides.
Add one or more @morph(...) directives to the owner model’s docblock to generate Laravel morph methods. Parentheses and quotes inside raw:"…" are supported.
Parameters
-
name:the morph base; supplies the string argument in Laravel calls and corresponds to the*_id/*_typepair on the child. -
type:one of:one→morphOnemany→morphManyto many→morphToManyby many→morphedByMany
-
model:target Eloquent model (class short name).
Optional
table:pivot table formorphToMany/morphedByMany.raw:chained expression appended to the relation, e.g.raw:"latest()->where('active',1)".as:method name override (defaults derive frommodel; pluralized for “many” types).idField:/typeField:to override the default<base>_id/<base>_typecolumn names on the child.
Examples
/// Owner: Post → morphMany(Comment::class, 'commentable') + chain
/// @morph(name: commentable, type: many, model: Comment, raw:"latest()", as: comments)
model Post {
id Int @id @default(autoincrement())
title String
}
/// Owner: User → morphOne(Image::class, 'imageable')
/// @morph(name: imageable, type: one, model: Image, as: avatar)
model User {
id Int @id @default(autoincrement())
name String
}
/// Owner: Video → morphToMany(Tag::class, 'taggable', 'taggables')
/// @morph(name: taggable, type: to many, model: Tag, table:"taggables")
model Video {
id Int @id @default(autoincrement())
url String
}Generated (sketch):
public function comments() { return $this->morphMany(Comment::class, 'commentable')->latest(); }
public function avatar() { return $this->morphOne(Image::class, 'imageable'); }
public function tags() { return $this->morphToMany(Tag::class, 'taggable', 'taggables'); }A) morphMany + child morphTo
/// @morph(name: commentable, type: many, model: Comment, raw:"latest()")
model Post {
id Int @id @default(autoincrement())
title String
}
model Comment {
id Int @id @default(autoincrement())
body String
commentable_id Int
commentable_type String
}B) morphOne + child morphTo
/// @morph(name: imageable, type: one, model: Image, as: avatar)
model User {
id Int @id @default(autoincrement())
name String
}
model Image {
id Int @id @default(autoincrement())
path String
imageable_id Int
imageable_type String
}C) Polymorphic M:N (morphToMany / morphedByMany)
/// @morph(name: taggable, type: to many, model: Tag, table: "taggables")
model Post {
id Int @id @default(autoincrement())
title String
}
/// @morph(name: taggable, type: to many, model: Tag, table: "taggables")
model Video {
id Int @id @default(autoincrement())
title String
url String
}
/// @morph(name: taggable, type: by many, model: Post, table: "taggables")
/// @morph(name: taggable, type: by many, model: Video, table: "taggables")
model Tag {
id Int @id @default(autoincrement())
name String @unique
}
// Optional explicit pivot table for integrity (Prisma treats polymorphic targets as scalars)
model Taggable {
id Int @id @default(autoincrement())
tag_id Int
taggable_id Int
taggable_type String
tag Tag @relation(fields: [tag_id], references: [id])
@@index([taggable_type, taggable_id])
@@map("taggables")
}D) Unconventional base name (actor)
/// @morph(name: actor, type: many, model: Activity, as: activities)
model User {
id Int @id @default(autoincrement())
email String @unique
}
model Activity {
id Int @id @default(autoincrement())
action String
actor_id Int
actor_type String
}/// @fillable{name,balance}
/// @hidden{secretToken}
model Account {
id Int @id @default(autoincrement())
balance Decimal @default(0.0) /// @cast{decimal:2}
nickname String /// @fillable @hidden
profile Json? /// @type{ import:'@types/forms', type:'ProfileDTO' }
company Company? @relation(fields:[companyId], references:[id]) /// @local
companyId Int?
posts Post[] /// @with
}
/// @with(posts,comments)
model User {
id Int @id @default(autoincrement())
email String
posts Post[]
comments Comment[]
}Generated (PHP, simplified)
protected $fillable = ['name','balance','nickname'];
protected $hidden = ['secretToken','nickname'];
protected $casts = ['balance' => 'decimal:2'];
public array $interfaces = [
'profile' => { import: '@types/forms', type: 'ProfileDTO' },
];
protected $with = ['posts'];@local prevents the company() relation method.
Combine multiple inline directives; they’re processed left‑to‑right.
/// @fillable{name,balance}
/// @hidden{secretToken}
/// @guarded{password,apiToken}
/// @trait:Illuminate\Auth\Authenticatable
/// @extend:Illuminate\Auth\User
/// @implements:Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract
/// @observer:App\Observers\UserObserver
/// @factory:UserFactory
/// @touch{company}
/// @appends{full_name}
model User {
id Int @id @default(autoincrement())
email String @unique /// @hidden @fillable
password String /// @hidden @guarded
balance Decimal @default(0.0) /// @cast{decimal:2}
profile Json? /// @type{ import:'@types/forms', type:'ProfileDTO' }
company Company? @relation(fields:[companyId], references:[id]) /// @local
companyId Int?
posts Post[] /// @with
}Generated output (simplified)
use Illuminate\Auth\Authenticatable;
use Illuminate\Auth\User;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Contracts\Auth\Authenticatable as AuthenticatableContract;
use App\Observers\UserObserver;
class User extends User implements AuthenticatableContract
{
use HasFactory, Authenticatable;
protected $fillable = ['name','balance','email'];
protected $hidden = ['secretToken','password'];
protected $guarded = ['password','apiToken'];
protected $casts = ['balance' => 'decimal:2'];
protected $touches = ['company'];
protected $appends = ['full_name'];
protected static string $factory = UserFactory::class;
protected static function boot()
{
parent::boot();
static::observe(UserObserver::class);
}
public function getFullNameAttribute()
{
return $this->attributes['full_name'] ?? null;
}
public function posts()
{
return $this->hasMany(Post::class);
}
}@localis model-method only; it does not change the migration. Use@silentto suppress a whole model/enum from emission.- Laravel cannot express composite pivot keys directly inside
belongsToMany(...). If your schema uses composite pivot keys, consider generating a Pivot model or using query/Compoships patterns on the one-to-many sides. - The generator keeps your edits safe via merge markers; conflicts will be surfaced with
<<<<<<<sections for manual resolution. - TS types are generated from the same DMMF + directives as PHP. You can always override a single field type with
@type{…}.
- Combine
migration&model&tsin onecustomizecommand when table names align. - Use
noEmit: truefor dry‑runs or CI validation. - Escape template chars in stub files.
- Keep your Prisma schema as the single source of truth; everything else is generated.
Use the library directly in a script or build tool instead of the CLI.
import {
generateLaravelSchema,
generateLaravelModels,
generateTypesFromPrisma,
sortMigrations,
} from 'prisma-laravel-migrate';
import { readFileSync, writeFileSync } from 'fs';
import { getDMMF } from '@prisma/sdk';
(async () => {
/* 1. Load schema & build DMMF */
const schemaPath = 'prisma/schema.prisma';
const datamodel = readFileSync(schemaPath, 'utf8');
const dmmf = await getDMMF({ datamodel });
/* 2. Run generators entirely in-memory */
const migrations = await generateLaravelSchema({
dmmf,
schemaPath, // ← always pass this
generator : { config: {} } as any,
});
const { models, enums } = await generateLaravelModels({
dmmf,
schemaPath,
generator : { config: {} } as any,
});
const tsResult = await generateTypesFromPrisma({
dmmf: dmmf as any,
schemaPath,
generator: {
config: {
// types-specific overrides
moduleName: 'database/prisma',
noEmit: true,
},
output: { value: 'resources/ts/prisma' },
} as any,
});
/* 3. Inspect or write output */
sortMigrations(migrations).forEach(m => {
writeFileSync(`./out/${m.tableName}.php`, m.statements.join('\n'), 'utf8');
});
// tsResult.models / tsResult.enums contain TS model/enum definitions
})();import { Rule } from 'prisma-laravel-migrate';
const softDeleteRule: Rule = {
test: d => d.name === 'deleted_at' && d.migrationType === 'timestamp',
render: () => ({
column : 'deleted_at',
snippet: ["$table->timestamp('deleted_at')->nullable();"],
}),
};
generateLaravelSchema({
dmmf,
schemaPath: 'prisma/schema.prisma',
generator : { config: { rules: [softDeleteRule] } } as any,
});| Export | Purpose |
|---|---|
generateLaravelSchema |
Build migration objects (and optionally write files) |
generateLaravelModels |
Build PHP model + enum definitions |
generateTypesFromPrisma |
Build TS model + enum definitions, with stub + module support |
sortMigrations |
Topologically sort migrations by FK dependencies |
Rule |
Type helper for custom migration shortcuts |
types (column-definition-types, laravel-config) |
Full TypeScript typings |
import {
ColumnDefinition,
LaravelSharedConfig,
MigratorConfigOverride,
ModelConfigOverride,
TypesConfigOverride,
} from 'prisma-laravel-migrate';Heads-up:
generateLaravelSchema,generateLaravelModels, andgenerateTypesFromPrismawrite files by default (honouring their respectiveoutputDirsettings). If you only want the in-memory objects—e.g. to capture the returnedmigrations,models,enums, or TSmodels/enumsarrays—setnoEmit: truein either
the per-call
generator.configobject:generateLaravelSchema({ dmmf, schemaPath, generator: { config: { noEmit: true } } as any, });or in
prisma/laravel.config.js:module.exports = { migrate: { noEmit: true }, modeler: { noEmit: true }, types: { noEmit: true }, };This prevents any files from being created or overwritten while still returning the fully-populated data structures for custom processing.
This document explains how prisma‑laravel‑migrate converts your Prisma schema into
Laravel migration code, so you can model your database in Prisma while still getting
clean, idiomatic Laravel migrations.
The generator first turns every Prisma (or native) scalar into the corresponding Laravel schema builder method.
| Native type | Laravel method |
|---|---|
Text |
text() |
VarChar |
string() |
Boolean |
boolean() |
TinyInt |
tinyInteger() |
UnsignedBigInt |
unsignedBigInteger() |
BigInt |
bigInteger() |
Decimal |
decimal() |
Double |
double() |
DateTime |
timestamp() |
Timestamptz |
timestampsTz() |
Json |
json() |
Uuid |
uuid() |
Inet |
ipAddress() |
(See source migrationTypes.ts for the full mapping.)
After basic mapping, each column is checked against a rule set so that common Laravel helper methods are used instead of verbose definitions.
| Rule | Condition in Prisma | Generated code |
|---|---|---|
| Primary ID | id BigInt @id @default(autoincrement()) |
$table->id(); |
| Timestamps | created_at / updated_at pair (DateTime or Timestamp) |
$table->timestamps(); |
| TimestampsTz | Same pair but DateTimeTz / tz‑aware types |
$table->timestampsTz(); |
| Soft‑deletes | deleted_at DateTime |
$table->softDeletes(); |
| Soft‑deletesTz | deleted_at DateTimeTz |
$table->softDeletesTz(); |
| Remember token | remember_token String? |
$table->rememberToken(); |
foreignId |
<col>_id + @relation(...) |
$table->foreignId('…')->constrained(); |
| Morphs / NullableMorphs | <base>_id & <base>_type combo |
$table->morphs('base'); / $table->nullableMorphs('base'); |
| UUID / ULID Morphs | Same but with Uuid / Ulid id column |
$table->uuidMorphs() & friends |
If a rule fires, both columns involved are marked ignored so the fallback builder doesn't emit them twice.
Anything that doesn't match a rule is rendered with:
$table->{method}('{name}', ...args)
->nullable() // if applicable
->unsigned() // if applicable
->default(...) // if applicable
->comment('…'); // if presentForeign‑key references (@relation) then add a separate $table->foreign() chain.
model Post {
id Int @id @default(autoincrement()) // ➜ $table->id()
title String
body Text
author_id Int
author User @relation(fields:[author_id], references:[id])
remember_token String? // ➜ rememberToken
created_at DateTime @default(now())
updated_at DateTime @updatedAt
deleted_at DateTime?
}$table->id();
$table->string('title');
$table->text('body');
$table->foreignId('author_id')->constrained('users', 'id');
$table->rememberToken();
$table->timestamps();
$table->softDeletes();- Native type hints – use
@db.VarChar(191)etc. Prisma passes the native type, the generator maps it to the proper Laravel builder. - Override or extend – supply your own
rulesarray inschema.prismato replace or add to the built‑in ones. - Stub files – tweak the surrounding PHP (imports, namespace, etc.) by editing
prisma/stubs/migration/index.stubor create per‑table stubs.
- Make columns nullable in Prisma if you expect
->nullable()in Laravel. - The generator relies on conventional column names (
*_id,created_at, …) for shortcut rules; non‑standard names fall back to the generic builder. - Remember to run migrations after generating to ensure FK order is correct (the tool topologically sorts tables to avoid dependency loops).
You can override the built‑in formatDefault() logic without forking the package by
passing a defaultMaps object in your generator (or shared) config.
// prisma/laravel.config.js
module.exports = {
migrate: {
// …
},
modeler: {
// …
},
/* 👇 NEW: map Prisma default names → your own formatter */
defaultMaps: {
uuid(field) {
// e.g. MSSQL `NEWID()`
return "->default(DB::raw('NEWID()'))";
},
cuid(field) {
// Use a DB function from a custom extension
return "->default(DB::raw('gen_cuid()'))";
},
// Fallback for any unmapped default remains the package default.
},
};How it works
- If
defaultMaps[name]exists, it is called with the currentDMMF.Field. - If the function returns a string, that string is appended directly to the column definition.
- If the function returns
null/undefined, the generator falls back to the built‑in behaviour. - Keys are matched case‑sensitive to the Prisma default function
(
uuid,cuid,sequence, etc.).
Reference – full list of Prisma
@@default()helpers https://www.prisma.io/docs/orm/reference/prisma-schema-reference#default
Happy scaffolding! 🎉
MIT — Happy scaffolding! 🎉