Schema as source of truth for Laravel migrations
Requires
- php: ^8.1
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- pestphp/pest: ^4
- pestphp/pest-plugin-laravel: ^4
- phpstan/phpstan: ^2.0
README
gboquizosanchez/hew
Schema as source of truth for Laravel migrations
Where is the avatar column defined? In migration number 47.
hew makes that a one-line answer.
Why hew?
Laravel migrations are append-only by design — great for versioning, painful for understanding the current state.
| Problem | Without hew | With hew |
|---|---|---|
"Does users have an avatar column?" |
❌ Read through N migrations | ✅ One glance at schema.php |
| Adding a column to an existing table | ❌ Write migration by hand | ✅ Add it to the schema, run hew:sync |
| Reviewing the full database structure | ❌ Mental model from git history | ✅ Single file, fluent API |
| CI check for uncommitted schema drift | ❌ No built-in mechanism | ✅ hew:diff exits 1 when changes pending |
| Accidental destructive migration | ❌ Easy to do | ✅ Impossible — hew never emits DROP |
🚀 Features
- Single source of truth:
database/schema.phpis the canonical description of your database. - Additive-only: hew never emits
DROP COLUMN,RENAME COLUMN, orDROP TABLE. Removed columns produce warnings, not data loss. - Fluent column API: Chainable modifiers —
->nullable(),->unique(),->default(),->references()and more. - Relation metadata: Declare
hasMany,belongsTo,belongsToManydirectly on the table definition.belongsToManyauto-generates the pivot table. - CI-friendly:
hew:diffexits1when changes are pending, making it trivially embeddable in pipelines. - Zero runtime footprint:
--devonly. No production dependency.
📦 Installation
composer require --dev gboquizosanchez/hew
php artisan vendor:publish --provider="Boquizo\Hew\HewServiceProvider"
cp vendor/gboquizosanchez/hew/database/schema.example.php database/schema.php
🔧 Usage
Define your schema
<?php // database/schema.php use Boquizo\Hew\Schema\Column; use Boquizo\Hew\Schema\Schema; use Boquizo\Hew\Schema\Table; return Schema::define([ Table::make('users') ->columns([ Column::id(), Column::string('name'), Column::string('email')->unique(), Column::string('password')->hidden(), Column::timestamps(), ]) ->hasMany('posts') ->belongsToMany('roles'), // auto-generates role_user pivot Table::make('posts') ->columns([ Column::id(), Column::foreignId('user_id')->references('users'), Column::string('title'), Column::text('body'), Column::boolean('is_published')->default(false), Column::decimal('reading_fee', 10, 2)->nullable(), Column::timestamps(), ]) ->belongsTo('users'), ]);
Check what would change
php artisan hew:diff
New table: posts
+ id
+ user_id, foreignId, -> users
+ title, string
+ body, text
+ is_published, boolean
+ reading_fee, decimal, 10,2, nullable
+ created_at, updated_at
No destructive changes will be applied automatically.
Generate the migrations
php artisan hew:sync
📋 Commands
hew:diff
Shows pending changes without writing any files. Exit code 0 if clean, 1 if changes are pending — useful in CI to detect uncommitted schema drift.
| Flag | Description |
|---|---|
--path |
Path to a non-default schema.php |
hew:sync
Runs the diff and generates migration files.
| Flag | Description |
|---|---|
--force |
Skip the [Y/n] confirmation prompt |
--dry-run |
Alias for hew:diff — show diff only, generate nothing |
--path |
Path to a non-default schema.php |
🗂️ Column reference
Types
| Method | Blueprint equivalent |
|---|---|
Column::id() |
$table->id() |
Column::string('name') |
$table->string('name') |
Column::text('body') |
$table->text('body') |
Column::integer('count') |
$table->integer('count') |
Column::bigInteger('amount') |
$table->bigInteger('amount') |
Column::decimal('total', 10, 2) |
$table->decimal('total', 10, 2) — precision and scale required |
Column::boolean('active') |
$table->boolean('active') |
Column::json('settings') |
$table->json('settings') |
Column::timestamp('paid_at') |
$table->timestamp('paid_at') |
Column::timestamps() |
$table->timestamps() — shortcut for created_at + updated_at |
Column::foreignId('user_id') |
$table->foreignId('user_id') |
Note:
Column::enum()is not supported and throwsUnsupportedColumnTypeException. UseColumn::string()->cast(YourEnum::class)instead — hew stores it asVARCHARin the database.
Modifiers
| Modifier | Blueprint effect | Notes |
|---|---|---|
->nullable() |
->nullable() |
|
->default($value) |
->default($value) |
|
->unique() |
->unique() |
|
->unsigned() |
->unsigned() |
|
->index() |
->index() |
|
->references('table') |
->constrained('table') |
Use on foreignId columns |
🔗 Table reference
Table::make('users') ->columns([...]) // required — array of Column instances ->hasMany('posts') // metadata only ->hasOne('profile') // metadata only ->belongsTo('companies') // warns if 'company_id' foreignId column is missing ->belongsToMany('roles') // auto-generates pivot table role_user if absent
All relation methods are metadata only — they don't generate columns or migrations by themselves.
belongsToMany('roles') on users automatically generates a role_user pivot table (alphabetical) with the two foreignId columns when none exists.
🔒 Safety guarantees
hew never emits DROP COLUMN, RENAME COLUMN, or DROP TABLE.
| Situation | What hew does |
|---|---|
| Column in schema, not in migrations | Generates migration to add it |
| Column in migrations, not in schema | Warning only — no migration |
| Column type changed | Warning only — no migration |
| Table in schema, not in migrations | Generates create_table migration |
When you see a warning, write that migration by hand. The cost is one extra file. The benefit is hew:sync can never destroy your data.
🧪 Testing
composer test
This package uses Pest v4 with the Laravel plugin. Static analysis runs via PHPStan at level 10.
Troubleshooting
If you encounter issues:
- Check the logs — Laravel logs may contain helpful error messages.
- Verify requirements — Ensure PHP ^8.1 and Laravel 10–13 are met.
- Clear cache — Run
php artisan config:clearandphp artisan cache:clear. - Open an issue — Report bugs or request features.
Contributing
Contributions are welcome! Please feel free to:
- 🐛 Report bugs via GitHub Issues
- 💡 Suggest features or improvements
- 🔧 Submit pull requests with bug fixes or enhancements
- 📖 Improve documentation or examples
Please make sure all tests pass and the code follows PSR-12 before submitting a PR.
Credits
- Author: Germán Boquizo Sánchez
- Contributors: View all contributors
📄 License
This package is open-source software licensed under the MIT License.
Made with ❤️ for the PHP · Laravel community