├── composer.json ├── config └── laravel-automatic-migrations.php ├── readme.md ├── resources └── stubs │ ├── Factory.php │ ├── Model.php │ ├── UserFactory.php │ └── UserModel.php └── src ├── Commands ├── MakeAModelCommand.php └── MigrateAutoCommand.php └── Providers └── LaravelAutomaticMigrationsProvider.php /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bastinald/laravel-automatic-migrations", 3 | "homepage": "https://github.com/bastinald/laravel-automatic-migrations", 4 | "description": "Automatic Laravel model migrations.", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Kevin Dion", 9 | "email": "bastinald@icloud.com", 10 | "role": "Developer" 11 | } 12 | ], 13 | "require": { 14 | "doctrine/dbal": "^2.0", 15 | "laravel/framework": "^8.0", 16 | "livewire/livewire": "^2.0" 17 | }, 18 | "autoload": { 19 | "psr-4": { 20 | "Bastinald\\LaravelAutomaticMigrations\\": "src" 21 | } 22 | }, 23 | "extra": { 24 | "laravel": { 25 | "providers": [ 26 | "Bastinald\\LaravelAutomaticMigrations\\Providers\\LaravelAutomaticMigrationsProvider" 27 | ] 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /config/laravel-automatic-migrations.php: -------------------------------------------------------------------------------- 1 | base_path('vendor/bastinald/laravel-automatic-migrations/resources/stubs'), 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Laravel Automatic Migrations 2 | 3 | Instead of having to create and manage migration files, this package allows you to specify your migrations inside your model classes via a `migration` method. When you run the `migrate:auto` command, it uses Doctrine to compare your model `migration` methods to the existing schema, and applies the changes automatically. 4 | 5 | This package works fine alongside traditional Laravel migration files, for the cases where you still need migrations that are not coupled to a model. When you run the `migrate:auto` command, it will run your traditional migrations first, and the automatic migrations afterwards. 6 | 7 | ## Documentation 8 | 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Commands](#commands) 12 | - [Making Models](#making-models) 13 | - [Running Migrations](#running-migrations) 14 | - [Migration Order](#migration-order) 15 | - [Publishing Stubs](#publishing-stubs) 16 | 17 | ## Installation 18 | 19 | Require the package via composer: 20 | 21 | ```console 22 | composer require bastinald/laravel-automatic-migrations 23 | ``` 24 | 25 | ## Usage 26 | 27 | Declare a `migration` method in your models: 28 | 29 | ```php 30 | namespace App\Models; 31 | 32 | use Illuminate\Database\Schema\Blueprint; 33 | use Illuminate\Database\Eloquent\Model; 34 | 35 | class MyModel extends Model 36 | { 37 | public function migration(Blueprint $table) 38 | { 39 | $table->id(); 40 | $table->string('name'); 41 | $table->timestamp('created_at')->nullable(); 42 | $table->timestamp('updated_at')->nullable(); 43 | } 44 | } 45 | ``` 46 | 47 | Run the `migrate:auto` command: 48 | 49 | ```console 50 | php artisan migrate:auto 51 | ``` 52 | 53 | ## Commands 54 | 55 | ### Making Models 56 | 57 | Make a model with a `migration` method included: 58 | 59 | ```console 60 | php artisan make:amodel {class} {--force} 61 | ``` 62 | 63 | This command will also make a factory whose `definition` points to the model method. Use `--force` to overwrite an existing model. 64 | 65 | ### Running Migrations 66 | 67 | Run automatic migrations: 68 | 69 | ```console 70 | php artisan migrate:auto {--f|--fresh} {--s|--seed} {--force} 71 | ``` 72 | 73 | Use `-f` to wipe the database, `-s` to seed after migration, and `--force` to run migrations in production. 74 | 75 | ## Migration Order 76 | 77 | You can specify the order to run your model migrations by adding a public `migrationOrder` property to your models. This is useful for pivot tables or situations where you must create a certain table before another. 78 | 79 | ```php 80 | class MyModel extends Model 81 | { 82 | public $migrationOrder = 1; 83 | 84 | public function migration(Blueprint $table) 85 | { 86 | $table->id(); 87 | $table->string('name'); 88 | $table->timestamp('created_at')->nullable(); 89 | $table->timestamp('updated_at')->nullable(); 90 | } 91 | } 92 | ``` 93 | 94 | The `migrate:auto` command will run the automatic migrations in the order specified. If no order is declared for a model, it will default to `0`. Thanks to [@vincentkedison](https://github.com/vincentkedison) for this idea. 95 | 96 | ## Publishing Stubs 97 | 98 | Use your own model and factory stubs by publishing package files: 99 | 100 | ```console 101 | php artisan vendor:publish --tag=laravel-automatic-migrations 102 | ``` 103 | 104 | Update the `stub_path` in `config/laravel-automatic-migrations.php`: 105 | 106 | ```php 107 | 'stub_path' => resource_path('stubs/vendor/laravel-automatic-migrations'), 108 | ``` 109 | 110 | Now edit the stub files inside `resources/stubs/vendor/laravel-automatic-migrations`. Commands will now use these stub files to make models and factories. 111 | -------------------------------------------------------------------------------- /resources/stubs/Factory.php: -------------------------------------------------------------------------------- 1 | model)->definition($this->faker); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /resources/stubs/Model.php: -------------------------------------------------------------------------------- 1 | id(); 19 | $table->string('name'); 20 | $table->timestamp('created_at')->nullable(); 21 | $table->timestamp('updated_at')->nullable(); 22 | } 23 | 24 | public function definition(Generator $faker) 25 | { 26 | return [ 27 | 'name' => $faker->name(), 28 | 'created_at' => $faker->dateTimeThisMonth(), 29 | ]; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /resources/stubs/UserFactory.php: -------------------------------------------------------------------------------- 1 | model)->definition($this->faker); 15 | } 16 | 17 | public function unverified() 18 | { 19 | return $this->state(function (array $attributes) { 20 | return [ 21 | 'email_verified_at' => null, 22 | ]; 23 | }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /resources/stubs/UserModel.php: -------------------------------------------------------------------------------- 1 | 'datetime']; 19 | 20 | public function migration(Blueprint $table) 21 | { 22 | $table->id(); 23 | $table->string('name'); 24 | $table->string('email')->unique(); 25 | $table->timestamp('email_verified_at')->nullable(); 26 | $table->string('password'); 27 | $table->rememberToken(); 28 | $table->timestamp('created_at')->nullable(); 29 | $table->timestamp('updated_at')->nullable(); 30 | } 31 | 32 | public function definition(Generator $faker) 33 | { 34 | return [ 35 | 'name' => $faker->name(), 36 | 'email' => $faker->unique()->safeEmail(), 37 | 'email_verified_at' => now(), 38 | 'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', 39 | 'remember_token' => Str::random(10), 40 | 'created_at' => $faker->dateTimeThisMonth(), 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Commands/MakeAModelCommand.php: -------------------------------------------------------------------------------- 1 | filesystem = new Filesystem; 20 | 21 | $this->modelParser = new ComponentParser( 22 | 'App\\Models', 23 | config('livewire.view_path'), 24 | $this->argument('class') 25 | ); 26 | 27 | $this->factoryParser = new ComponentParser( 28 | 'Database\\Factories', 29 | config('livewire.view_path'), 30 | $this->argument('class') . 'Factory' 31 | ); 32 | 33 | if ($this->filesystem->exists($this->modelParser->classPath()) && !$this->option('force')) { 34 | $this->line('Model exists: ' . $this->modelParser->relativeClassPath()); 35 | $this->warn('Use the --force to overwrite it.'); 36 | 37 | return; 38 | } 39 | 40 | $this->deleteUserMigration(); 41 | $this->makeStubs(); 42 | 43 | $this->line('Model created: ' . $this->modelParser->relativeClassPath()); 44 | $this->line('Factory created: ' . $this->factoryPath('relativeClassPath')); 45 | } 46 | 47 | private function deleteUserMigration() 48 | { 49 | if ($this->modelParser->className() != 'User') { 50 | return; 51 | } 52 | 53 | $path = 'database/migrations/2014_10_12_000000_create_users_table.php'; 54 | $file = base_path($path); 55 | 56 | if ($this->filesystem->exists($file)) { 57 | $this->filesystem->delete($file); 58 | 59 | $this->line('Migration deleted: ' . $path); 60 | } 61 | } 62 | 63 | private function makeStubs() 64 | { 65 | $prefix = $this->modelParser->className() == 'User' ? 'User' : null; 66 | 67 | $stubs = [ 68 | $this->modelParser->classPath() => $prefix . 'Model.php', 69 | $this->factoryPath('classPath') => $prefix . 'Factory.php', 70 | ]; 71 | 72 | $replaces = [ 73 | 'DummyFactoryClass' => $this->factoryParser->className(), 74 | 'DummyFactoryNamespace' => $this->factoryParser->classNamespace(), 75 | 'DummyModelClass' => $this->modelParser->className(), 76 | 'DummyModelNamespace' => $this->modelParser->classNamespace(), 77 | ]; 78 | 79 | foreach ($stubs as $path => $stub) { 80 | $contents = Str::replace( 81 | array_keys($replaces), 82 | $replaces, 83 | $this->filesystem->get(config('laravel-automatic-migrations.stub_path') . '/' . $stub) 84 | ); 85 | 86 | $this->filesystem->ensureDirectoryExists(dirname($path)); 87 | $this->filesystem->put($path, $contents); 88 | } 89 | } 90 | 91 | private function factoryPath($method) 92 | { 93 | return Str::replaceFirst( 94 | 'app/Database/Factories', 95 | 'database/factories', 96 | $this->factoryParser->$method() 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Commands/MigrateAutoCommand.php: -------------------------------------------------------------------------------- 1 | environment('production') && !$this->option('force')) { 20 | $this->warn('Use the --force to migrate in production.'); 21 | 22 | return; 23 | } 24 | 25 | $this->handleTraditionalMigrations(); 26 | $this->handleAutomaticMigrations(); 27 | $this->seed(); 28 | 29 | $this->info('Automatic migration completed successfully.'); 30 | } 31 | 32 | private function handleTraditionalMigrations() 33 | { 34 | $command = 'migrate'; 35 | 36 | if ($this->option('fresh')) { 37 | $command .= ':fresh'; 38 | } 39 | 40 | if ($this->option('force')) { 41 | $command .= ' --force'; 42 | } 43 | 44 | Artisan::call($command, [], $this->getOutput()); 45 | } 46 | 47 | private function handleAutomaticMigrations() 48 | { 49 | $path = app_path('Models'); 50 | $namespace = app()->getNamespace(); 51 | $models = collect(); 52 | 53 | if (!is_dir($path)) { 54 | return; 55 | } 56 | 57 | foreach ((new Finder)->in($path) as $model) { 58 | $model = $namespace . str_replace( 59 | ['/', '.php'], 60 | ['\\', ''], 61 | Str::after($model->getRealPath(), realpath(app_path()) . DIRECTORY_SEPARATOR) 62 | ); 63 | 64 | if (method_exists($model, 'migration')) { 65 | $models->push([ 66 | 'object' => $object = app($model), 67 | 'order' => $object->migrationOrder ?? 0, 68 | ]); 69 | } 70 | } 71 | 72 | foreach ($models->sortBy('order') as $model) { 73 | $this->migrate($model['object']); 74 | } 75 | } 76 | 77 | private function migrate($model) 78 | { 79 | $modelTable = $model->getTable(); 80 | $tempTable = 'table_' . $modelTable; 81 | 82 | Schema::dropIfExists($tempTable); 83 | Schema::create($tempTable, function (Blueprint $table) use ($model) { 84 | $model->migration($table); 85 | }); 86 | 87 | if (Schema::hasTable($modelTable)) { 88 | $schemaManager = $model->getConnection()->getDoctrineSchemaManager(); 89 | $modelTableDetails = $schemaManager->listTableDetails($modelTable); 90 | $tempTableDetails = $schemaManager->listTableDetails($tempTable); 91 | $tableDiff = (new Comparator)->diffTable($modelTableDetails, $tempTableDetails); 92 | 93 | if ($tableDiff) { 94 | $schemaManager->alterTable($tableDiff); 95 | 96 | $this->line('Table updated: ' . $modelTable); 97 | } 98 | 99 | Schema::drop($tempTable); 100 | } else { 101 | Schema::rename($tempTable, $modelTable); 102 | 103 | $this->line('Table created: ' . $modelTable); 104 | } 105 | } 106 | 107 | private function seed() 108 | { 109 | if (!$this->option('seed')) { 110 | return; 111 | } 112 | 113 | $command = 'db:seed'; 114 | 115 | if ($this->option('force')) { 116 | $command .= ' --force'; 117 | } 118 | 119 | Artisan::call($command, [], $this->getOutput()); 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Providers/LaravelAutomaticMigrationsProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 14 | $this->commands([ 15 | MakeAModelCommand::class, 16 | MigrateAutoCommand::class, 17 | ]); 18 | } 19 | 20 | $this->publishes( 21 | [__DIR__ . '/../../config/laravel-automatic-migrations.php' => config_path('laravel-automatic-migrations.php')], 22 | ['laravel-automatic-migrations', 'laravel-automatic-migrations:config'] 23 | ); 24 | 25 | $this->publishes( 26 | [__DIR__ . '/../../resources/stubs' => resource_path('stubs/vendor/laravel-automatic-migrations')], 27 | ['laravel-automatic-migrations', 'laravel-automatic-migrations:stubs'] 28 | ); 29 | } 30 | 31 | public function register() 32 | { 33 | $this->mergeConfigFrom(__DIR__ . '/../../config/laravel-automatic-migrations.php', 'laravel-automatic-migrations'); 34 | } 35 | } 36 | --------------------------------------------------------------------------------