├── 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 |
--------------------------------------------------------------------------------