├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ └── bug_report.md └── workflows │ └── php.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── UPGRADE.md ├── composer.json ├── config └── laravel-migration-generator.php ├── docs ├── _config.yml ├── command.md ├── config.md ├── index.md └── stubs.md ├── phpunit.xml ├── pint.json ├── src ├── Commands │ └── GenerateMigrationsCommand.php ├── Definitions │ ├── ColumnDefinition.php │ ├── IndexDefinition.php │ ├── TableDefinition.php │ └── ViewDefinition.php ├── Formatters │ ├── TableFormatter.php │ └── ViewFormatter.php ├── GeneratorManagers │ ├── BaseGeneratorManager.php │ ├── Interfaces │ │ └── GeneratorManagerInterface.php │ └── MySQLGeneratorManager.php ├── Generators │ ├── BaseTableGenerator.php │ ├── BaseViewGenerator.php │ ├── Concerns │ │ ├── CleansUpColumnIndices.php │ │ ├── CleansUpForeignKeyIndices.php │ │ ├── CleansUpMorphColumns.php │ │ ├── CleansUpTimestampsColumn.php │ │ ├── WritesToFile.php │ │ └── WritesViewsToFile.php │ ├── Interfaces │ │ ├── TableGeneratorInterface.php │ │ └── ViewGeneratorInterface.php │ └── MySQL │ │ ├── TableGenerator.php │ │ └── ViewGenerator.php ├── Helpers │ ├── ConfigResolver.php │ ├── DependencyResolver.php │ ├── Formatter.php │ ├── ValueToString.php │ └── WritableTrait.php ├── LaravelMigrationGeneratorProvider.php └── Tokenizers │ ├── BaseColumnTokenizer.php │ ├── BaseIndexTokenizer.php │ ├── BaseTokenizer.php │ ├── Interfaces │ ├── ColumnTokenizerInterface.php │ └── IndexTokenizerInterface.php │ └── MySQL │ ├── ColumnTokenizer.php │ └── IndexTokenizer.php ├── stubs ├── table-create.stub ├── table-modify.stub ├── table.stub └── view.stub └── tests ├── TestCase.php └── Unit ├── ColumnDefinitionTest.php ├── DependencyResolverTest.php ├── FormatterTest.php ├── GeneratorManagers └── MySQLGeneratorManagerTest.php ├── Generators ├── MySQLTableGeneratorTest.php └── MySQLViewGeneratorTest.php └── Tokenizers └── MySQL ├── ColumnTokenizerTest.php └── IndexTokenizerTest.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [bennett-treptow] 4 | custom: ['https://buymeacoffee.com/btreptow'] 5 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Package Version** 11 | What version are you running? 2.2.*, 3.* 12 | 13 | **Database Version** 14 | What database driver are you using? And what version is that database? 15 | 16 | **Describe the bug** 17 | A clear and concise description of what the bug is. 18 | 19 | **To Reproduce** 20 | Please include any stack traces and applicable .env / config changes you've made 21 | 22 | **Expected behavior** 23 | A clear and concise description of what you expected to happen. 24 | 25 | **Screenshots** 26 | If applicable, add screenshots to help explain your problem. 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: PHP Composer 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | strategy: 14 | matrix: 15 | php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | 26 | - name: Cache Composer packages 27 | id: composer-cache 28 | uses: actions/cache@v4 29 | with: 30 | path: vendor 31 | key: "${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}" 32 | restore-keys: | 33 | ${{ runner.os }}-php-${{ matrix.php }}- 34 | 35 | - name: Remove pint if not PHP 8.1 36 | run: | 37 | if [ '${{ matrix.php }}' != '8.1' ]; then 38 | composer remove laravel/pint --dev 39 | fi 40 | 41 | - name: Install dependencies 42 | run: composer install --prefer-dist --no-progress 43 | 44 | - name: Run test suite 45 | run: composer run test 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor/ 2 | .idea 3 | .phpunit.result.cache 4 | .php_cs.cache 5 | composer.lock -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Version 3.1.6 2 | ### New Modifier 3 | `useCurrentOnUpdate` has been implemented 4 | ### Bugfix 5 | Issue #27 - `useCurrent` on `timestamps()` method fix 6 | 7 | # Version 3.1.3 8 | ### [Timestamp:format] Removal 9 | The [Timestamp:format] token for file names has been removed. Migration file names require that [Timestamp] be at the beginning in that specific format. Any other format would cause the migrations to not be loaded. 10 | 11 | 12 | # Version 3.1.0 13 | ### Environment Variables 14 | New environment variables: 15 | 16 | | Key | Default Value | Allowed Values | Description | 17 | | --- | ------------- | -------------- | ----------- | 18 | | LMG_SKIP_VIEWS | false | boolean | When true, skip all views | 19 | | LMG_SKIPPABLE_VIEWS | '' | comma delimited string | The views to be skipped | 20 | | LMG_MYSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `mysql` | 21 | | LMG_SQLITE_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlite` | 22 | | LMG_PGSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `pgsql` | 23 | | LMG_SQLSRV_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlsrv` | 24 | 25 | # Version 3.0.0 26 | 27 | ### Run after migrations 28 | When `LMG_RUN_AFTER_MIGRATIONS` is set to true, after running any of the `artisan migrate` commands, the `generate:migrations` command will be run using all the default options for the command. It will only run when the app environment is `local`. 29 | 30 | ### Environment Variables 31 | New environment variables to replace config updates: 32 | 33 | | Key | Default Value | Allowed Values | Description | 34 | | --- | ------------- | -------------- | ----------- | 35 | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | 36 | | LMG_CLEAR_OUTPUT_PATH | false | boolean | Whether or not to clear out the output path before creating new files | 37 | | LMG_TABLE_NAMING_SCHEME | [Timestamp]_create_[TableName]_table.php | string | The string to be used to name table migration files | 38 | | LMG_VIEW_NAMING_SCHEME | [Timestamp]_create_[ViewName]_view.php | string | The string to be used to name view migration files | 39 | | LMG_OUTPUT_PATH | tests/database/migrations | string | The path (relative to the root of your project) to where the files will be output to | 40 | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | 41 | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | 42 | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | 43 | | LMG_USE_DEFINED_FOREIGN_KEY_INDEX_NAMES | true | boolean | When true, uses foreign key index names defined by the database as the name parameter for foreign key methods | 44 | | LMG_USE_DEFINED_UNIQUE_KEY_INDEX_NAMES | true | boolean | When true, uses unique key index names defined by the database as the name parameter for the `unique` methods | 45 | | LMG_USE_DEFINED_PRIMARY_KEY_INDEX_NAMES | true | boolean | When true, uses primary key index name defined by the database as the name parameter for the `primary` method | 46 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | 47 | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | 48 | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | 49 | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | 50 | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | 51 | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | 52 | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | 53 | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | 54 | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | 55 | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | 56 | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | 57 | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | 58 | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | 59 | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | 60 | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | 61 | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Bennett Treptow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Migration Generator 2 | ![Latest Version on Packagist](https://img.shields.io/packagist/v/bennett-treptow/laravel-migration-generator.svg) 3 | 4 | Generate migrations from existing database structures, an alternative to the schema dump provided by Laravel. A primary use case for this package would be a project that has many migrations that alter tables using `->change()` from doctrine/dbal that SQLite doesn't support and need a way to get table structures updated for SQLite to use in tests. 5 | Another use case would be taking a project with a database and no migrations and turning that database into base migrations. 6 | 7 | # Installation 8 | ```bash 9 | composer require --dev bennett-treptow/laravel-migration-generator 10 | ``` 11 | 12 | ```bash 13 | php artisan vendor:publish --provider="LaravelMigrationGenerator\LaravelMigrationGeneratorProvider" 14 | ``` 15 | # Lumen Installation 16 | ```bash 17 | composer require --dev bennett-treptow/laravel-migration-generator 18 | ``` 19 | 20 | Copy config file from `vendor/bennett-treptow/laravel-migration-generator/config` to your Lumen config folder 21 | 22 | Register service provider in bootstrap/app.php 23 | ```php 24 | $app->register(\LaravelMigrationGenerator\LaravelMigrationGeneratorProvider::class); 25 | ``` 26 | 27 | # Usage 28 | 29 | Whenever you have database changes or are ready to squash your database structure down to migrations, run: 30 | ```bash 31 | php artisan generate:migrations 32 | ``` 33 | 34 | By default, the migrations will be created in `tests/database/migrations`. You can specify a different path with the `--path` option: 35 | ```bash 36 | php artisan generate:migrations --path=database/migrations 37 | ``` 38 | 39 | You can specify the connection to use as the database with the `--connection` option: 40 | ```bash 41 | php artisan generate:migrations --connection=mysql2 42 | ``` 43 | 44 | You can also clear the directory with the `--empty-path` option: 45 | ```bash 46 | php artisan generate:migrations --empty-path 47 | ``` 48 | 49 | This command can also be run by setting the `LMG_RUN_AFTER_MIGRATIONS` environment variable to `true` and running your migrations as normal. This will latch into the `MigrationsEnded` event and run this command using the default options specified via your environment variables. Note: it will only run when your app environment is set to `local`. 50 | 51 | # Configuration 52 | 53 | Want to customize the migration stubs? Make sure you've published the vendor assets with the artisan command to publish vendor files above. 54 | 55 | ## Environment Variables 56 | 57 | | Key | Default Value | Allowed Values | Description | 58 | | --- | ------------- | -------------- | ----------- | 59 | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | 60 | | LMG_CLEAR_OUTPUT_PATH | false | boolean | Whether or not to clear out the output path before creating new files. Same as specifying `--empty-path` on the command | 61 | | LMG_TABLE_NAMING_SCHEME | `[Timestamp]_create_[TableName]_table.php` | string | The string to be used to name table migration files | 62 | | LMG_VIEW_NAMING_SCHEME | `[Timestamp]_create_[ViewName]_view.php` | string | The string to be used to name view migration files | 63 | | LMG_OUTPUT_PATH | tests/database/migrations | string | The path (relative to the root of your project) to where the files will be output to. Same as specifying `--path=` on the command | 64 | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | 65 | | LMG_SKIP_VIEWS | false | boolean | When true, skip all views | 66 | | LMG_SKIPPABLE_VIEWS | '' | comma delimited string | The views to be skipped | 67 | | LMG_SORT_MODE | 'foreign_key' | string | The sorting mode to be used. Options: `foreign_key` | 68 | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | 69 | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | 70 | | LMG_USE_DEFINED_FOREIGN_KEY_INDEX_NAMES | true | boolean | When true, uses foreign key index names defined by the database as the name parameter for foreign key methods | 71 | | LMG_USE_DEFINED_UNIQUE_KEY_INDEX_NAMES | true | boolean | When true, uses unique key index names defined by the database as the name parameter for the `unique` methods | 72 | | LMG_USE_DEFINED_PRIMARY_KEY_INDEX_NAMES | true | boolean | When true, uses primary key index name defined by the database as the name parameter for the `primary` method | 73 | | LMG_WITH_COMMENTS | true | boolean | When true, export comment using `->comment()` method. | 74 | | LMG_USE_DEFINED_DATATYPE_ON_TIMESTAMP | false | boolean | When false, uses `->timestamps()` by mashing up `created_at` and `updated_at` regardless of datatype defined by the database | 75 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | 76 | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | 77 | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | 78 | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | 79 | | LMG_MYSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `mysql` | 80 | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | 81 | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | 82 | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | 83 | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | 84 | | LMG_SQLITE_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlite` | 85 | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | 86 | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | 87 | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | 88 | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | 89 | | LMG_PGSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `pgsql` | 90 | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | 91 | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | 92 | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | 93 | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | 94 | | LMG_SQLSRV_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlsrv` | 95 | 96 | ## Stubs 97 | There is a default stub for tables and views, found in `resources/stubs/vendor/laravel-migration-generator/`. 98 | Each database driver can be assigned a specific migration stub by creating a new stub file in `resources/stubs/vendor/laravel-migration-generator/` with a `driver`-prefix, e.g. `mysql-table.stub` for a MySQL specific table stub. 99 | 100 | ## Stub Naming 101 | Table and view stubs can be named using the `LMG_(TABLE|VIEW)_NAMING_SCHEME` environment variables. Optionally, driver-specific naming schemes can be used as well by specifying `LMG_{driver}_TABLE_NAMING_SCHEME` environment vars using the same tokens. See below for available tokens that can be replaced. 102 | 103 | ### Table Name Stub Tokens 104 | Table stubs have the following tokens available for the naming scheme: 105 | 106 | | Token | Example | Description | 107 | | ----- |-------- | ----------- | 108 | | `[TableName]` | users | Table's name, same as what is defined in the database | 109 | | `[TableName:Studly]` | Users | Table's name with `Str::studly()` applied to it (useful for standardizing table names if they are inconsistent) | 110 | | `[TableName:Lowercase]` | users | Table's name with `strtolower` applied to it (useful for standardizing table names if they are inconsistent) | 111 | | `[Timestamp]` | 2021_04_25_110000 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` | 112 | | `[Index]` | 0 | The key of the migration in the sorted order, for use with enforcing a sort order | 113 | | `[IndexedEmptyTimestamp]` | 0000_00_00_000041 | The standard migration timestamp format, but filled with 0s and incremented by `[Index]` seconds | 114 | | `[IndexedTimestamp]` | 2021_04_25_110003 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` incremented by `[Index]` seconds | 115 | 116 | 117 | ### Table Schema Stub Tokens 118 | Table schema stubs have the following tokens available: 119 | 120 | | Token | Description | 121 | | ----- | ----------- | 122 | | `[TableName]` | Table's name, same as what is defined in the database | 123 | | `[TableName:Studly]` | Table's name with `Str::studly()` applied to it, for use with the class name | 124 | | `[TableUp]` | Table's `up()` function | 125 | | `[TableDown]` | Table's `down()` function | 126 | | `[Schema]` | The table's generated schema | 127 | 128 | 129 | ### View Name Stub Tokens 130 | View stubs have the following tokens available for the naming scheme: 131 | 132 | | Token | Example | Description | 133 | | ----- |-------- | ----------- | 134 | | `[ViewName]` | user_earnings | View's name, same as what is defined in the database | 135 | | `[ViewName:Studly]` | UserEarnings | View's name with `Str::studly()` applied to it (useful for standardizing view names if they are inconsistent) | 136 | | `[ViewName:Lowercase]` | user_earnings | View's name with `strtolower` applied to it (useful for standardizing view names if they are inconsistent) | 137 | | `[Timestamp]` | 2021_04_25_110000 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` | 138 | | `[Index]` | 0 | The key of the migration in the sorted order, for use with enforcing a sort order | 139 | | `[IndexedEmptyTimestamp]` | 0000_00_00_000041 | The standard migration timestamp format, but filled with 0s and incremented by `[Index]` seconds | 140 | | `[IndexedTimestamp]` | 2021_04_25_110003 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` incremented by `[Index]` seconds | 141 | 142 | ### View Schema Stub Tokens 143 | View schema stubs have the following tokens available: 144 | 145 | | Token | Description | 146 | | ----- | ----------- | 147 | | `[ViewName]` | View's name, same as what is defined in the database | 148 | | `[ViewName:Studly]` | View's name with `Str::studly()` applied to it, for use with the class name | 149 | | `[Schema]` | The view's schema | 150 | 151 | 152 | # Example Usage 153 | 154 | Given a database structure for a `users` table of: 155 | ```sql 156 | CREATE TABLE `users` ( 157 | `id` int(10) unsigned NOT NULL AUTO_INCREMENT, 158 | `username` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 159 | `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 160 | `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, 161 | `first_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 162 | `last_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 163 | `timezone` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'America/New_York', 164 | `location_id` int(10) unsigned NOT NULL, 165 | `deleted_at` timestamp NULL DEFAULT NULL, 166 | `remember_token` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, 167 | `created_at` timestamp NULL DEFAULT NULL, 168 | `updated_at` timestamp NULL DEFAULT NULL, 169 | PRIMARY KEY (`id`), 170 | KEY `users_username_index` (`username`), 171 | KEY `users_first_name_index` (`first_name`), 172 | KEY `users_last_name_index` (`last_name`), 173 | KEY `users_email_index` (`email`), 174 | KEY `fk_users_location_id_index` (`location_id`) 175 | CONSTRAINT `users_location_id_foreign` FOREIGN KEY (`location_id`) REFERENCES `locations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE 176 | ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci 177 | ``` 178 | 179 | A `tests/database/migrations/[TIMESTAMP]_create_users_table.php` with the following Blueprint would be created: 180 | ```php 181 | increments('id'); 198 | $table->string('username', 128)->nullable()->index(); 199 | $table->string('email', 255)->index(); 200 | $table->string('password', 255); 201 | $table->string('first_name', 45)->nullable()->index(); 202 | $table->string('last_name', 45)->index(); 203 | $table->string('timezone', 45)->default('America/New_York'); 204 | $table->unsignedInteger('location_id'); 205 | $table->softDeletes(); 206 | $table->string('remember_token', 255)->nullable(); 207 | $table->timestamps(); 208 | $table->foreign('location_id', 'users_location_id_foreign')->references('id')->on('locations')->onUpdate('cascade')->onDelete('cascade'); 209 | }); 210 | } 211 | 212 | /** 213 | * Reverse the migrations. 214 | * 215 | * @return void 216 | */ 217 | public function down() 218 | { 219 | Schema::dropIfExists('users'); 220 | } 221 | } 222 | ``` 223 | 224 | 225 | # Currently Supported DBMS's 226 | These DBMS's are what are currently supported for creating migrations **from**. Migrations created will, as usual, follow what [database drivers Laravel migrations allow for](https://laravel.com/docs/8.x/database#introduction) 227 | 228 | - [x] MySQL 229 | - [ ] Postgres 230 | - [ ] SQLite 231 | - [ ] SQL Server 232 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | ## Upgrade from 3.* to 4.0 2 | 3 | ### Foreign Key Sorting 4 | New foreign key dependency sorting options, available as an env variable to potentially not sort by foreign key dependencies if not necessary. 5 | Update your `config/laravel-migration-generator.php` to have a new `sort_mode` key: 6 | 7 | ```dotenv 8 | 'sort_mode' => env('LMG_SORT_MODE', 'foreign_key'), 9 | ``` 10 | 11 | ### New Stubs 12 | New stubs for a `create` and a `modify` version for tables. 13 | If you want to change how a `Schema::create` or `Schema::table` is output as, create a new `table-create.stub` or `table-modify.stub` and their driver variants as well if desired. 14 | 15 | ### New Table and View Naming Tokens 16 | 17 | Three new tokens were added for the table stubs: `[Index]`, `[IndexedEmptyTimestamp]`, and `[IndexedTimestamp]`. 18 | For use with foreign key / sorting in general to enforce a final sorting. 19 | 20 | `[Index]` is the numeric key (0,1,2,...) that the migration holds in the sort order. 21 | 22 | `[IndexedEmptyTimestamp]` is the `[Index]` but prefixed with the necessary digits and underscores for the file to be recognized as a migration. `0000_00_00_000001_migration.php` 23 | 24 | `[IndexedTimestamp]` is the current time incremented by `[Index]` seconds. So first migration would be the current time, second migration would be +1 second, third +2 seconds, etc. 25 | 26 | ### New Table Stub Tokens 27 | Two new tokens were added for table stubs: `[TableUp]` and `[TableDown]`. 28 | See latest `stubs/table.stub`. It is suggested to upgrade all of your stubs using the latest stubs available by `vendor:publish --force` 29 | 30 | ## Upgrade from 2.2.* to 3.0.0 31 | 32 | `skippable_tables` is now a comma delimited string instead of an array so they are compatible with .env files. 33 | 34 | All config options have been moved to equivalent .env variables. Please update `config/laravel-migration-generator.php` with a `vendor:publish --force`. 35 | The new environment variables are below: 36 | 37 | | Key | Default Value | Allowed Values | Description | 38 | | --- | ------------- | -------------- | ----------- | 39 | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | 40 | | LMG_CLEAR_OUTPUT_PATH | false | boolean | Whether or not to clear out the output path before creating new files | 41 | | LMG_TABLE_NAMING_SCHEME | [Timestamp]_create_[TableName]_table.php | string | The string to be used to name table migration files | 42 | | LMG_VIEW_NAMING_SCHEME | [Timestamp]_create_[ViewName]_view.php | string | The string to be used to name view migration files | 43 | | LMG_OUTPUT_PATH | tests/database/migrations | string | The path (relative to the root of your project) to where the files will be output to | 44 | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | 45 | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | 46 | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | 47 | | LMG_USE_DEFINED_FOREIGN_KEY_INDEX_NAMES | true | boolean | When true, uses foreign key index names defined by the database as the name parameter for foreign key methods | 48 | | LMG_USE_DEFINED_UNIQUE_KEY_INDEX_NAMES | true | boolean | When true, uses unique key index names defined by the database as the name parameter for the `unique` methods | 49 | | LMG_USE_DEFINED_PRIMARY_KEY_INDEX_NAMES | true | boolean | When true, uses primary key index name defined by the database as the name parameter for the `primary` method | 50 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | 51 | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | 52 | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | 53 | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | 54 | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | 55 | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | 56 | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | 57 | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | 58 | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | 59 | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | 60 | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | 61 | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | 62 | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | 63 | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | 64 | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | 65 | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | 66 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bennett-treptow/laravel-migration-generator", 3 | "description": "Generate migrations from existing database structures", 4 | "minimum-stability": "stable", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Bennett Treptow", 9 | "email": "me@btreptow.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.4|^8.0|^8.1|^8.2|^8.3|^8.4", 14 | "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 15 | "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 16 | "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 17 | "illuminate/config": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", 18 | "marcj/topsort": "^2.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "LaravelMigrationGenerator\\": "src/" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "Tests\\": "tests/" 28 | } 29 | }, 30 | "require-dev": { 31 | "orchestra/testbench": "^6.17|^8.0|^9.0|^10.0", 32 | "laravel/pint": "^1.15" 33 | }, 34 | "scripts": { 35 | "post-autoload-dump": [ 36 | "@php ./vendor/bin/testbench package:discover --ansi" 37 | ], 38 | "test": [ 39 | "vendor/bin/phpunit" 40 | ], 41 | "lint": "vendor/bin/pint" 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "LaravelMigrationGenerator\\LaravelMigrationGeneratorProvider" 47 | ] 48 | } 49 | }, 50 | "prefer-stable": true 51 | } 52 | -------------------------------------------------------------------------------- /config/laravel-migration-generator.php: -------------------------------------------------------------------------------- 1 | env('LMG_RUN_AFTER_MIGRATIONS', false), 5 | 'clear_output_path' => env('LMG_CLEAR_OUTPUT_PATH', false), 6 | //default configs 7 | 'table_naming_scheme' => env('LMG_TABLE_NAMING_SCHEME', '[IndexedTimestamp]_create_[TableName]_table.php'), 8 | 'view_naming_scheme' => env('LMG_VIEW_NAMING_SCHEME', '[IndexedTimestamp]_create_[ViewName]_view.php'), 9 | 'path' => env('LMG_OUTPUT_PATH', 'tests/database/migrations'), 10 | 'skippable_tables' => env('LMG_SKIPPABLE_TABLES', 'migrations'), 11 | 'skip_views' => env('LMG_SKIP_VIEWS', false), 12 | 'skippable_views' => env('LMG_SKIPPABLE_VIEWS', ''), 13 | 'sort_mode' => env('LMG_SORT_MODE', 'foreign_key'), 14 | 'definitions' => [ 15 | 'prefer_unsigned_prefix' => env('LMG_PREFER_UNSIGNED_PREFIX', true), 16 | 'use_defined_index_names' => env('LMG_USE_DEFINED_INDEX_NAMES', true), 17 | 'use_defined_foreign_key_index_names' => env('LMG_USE_DEFINED_FOREIGN_KEY_INDEX_NAMES', true), 18 | 'use_defined_unique_key_index_names' => env('LMG_USE_DEFINED_UNIQUE_KEY_INDEX_NAMES', true), 19 | 'use_defined_primary_key_index_names' => env('LMG_USE_DEFINED_PRIMARY_KEY_INDEX_NAMES', true), 20 | 'with_comments' => env('LMG_WITH_COMMENTS', true), 21 | 'use_defined_datatype_on_timestamp' => env('LMG_USE_DEFINED_DATATYPE_ON_TIMESTAMP', false), 22 | ], 23 | 24 | //now driver specific configs 25 | //null = use default 26 | 'mysql' => [ 27 | 'table_naming_scheme' => env('LMG_MYSQL_TABLE_NAMING_SCHEME', null), 28 | 'view_naming_scheme' => env('LMG_MYSQL_VIEW_NAMING_SCHEME', null), 29 | 'path' => env('LMG_MYSQL_OUTPUT_PATH', null), 30 | 'skippable_tables' => env('LMG_MYSQL_SKIPPABLE_TABLES', null), 31 | 'skippable_views' => env('LMG_MYSQL_SKIPPABLE_VIEWS', null), 32 | ], 33 | 'sqlite' => [ 34 | 'table_naming_scheme' => env('LMG_SQLITE_TABLE_NAMING_SCHEME', null), 35 | 'view_naming_scheme' => env('LMG_SQLITE_VIEW_NAMING_SCHEME', null), 36 | 'path' => env('LMG_SQLITE_OUTPUT_PATH', null), 37 | 'skippable_tables' => env('LMG_SQLITE_SKIPPABLE_TABLES', null), 38 | 'skippable_views' => env('LMG_SQLITE_SKIPPABLE_VIEWS', null), 39 | ], 40 | 'pgsql' => [ 41 | 'table_naming_scheme' => env('LMG_PGSQL_TABLE_NAMING_SCHEME', null), 42 | 'view_naming_scheme' => env('LMG_PGSQL_VIEW_NAMING_SCHEME', null), 43 | 'path' => env('LMG_PGSQL_OUTPUT_PATH', null), 44 | 'skippable_tables' => env('LMG_PGSQL_SKIPPABLE_TABLES', null), 45 | 'skippable_views' => env('LMG_PGSQL_SKIPPABLE_VIEWS', null), 46 | ], 47 | 'sqlsrv' => [ 48 | 'table_naming_scheme' => env('LMG_SQLSRV_TABLE_NAMING_SCHEME', null), 49 | 'view_naming_scheme' => env('LMG_SQLSRV_VIEW_NAMING_SCHEME', null), 50 | 'path' => env('LMG_SQLSRV_OUTPUT_PATH', null), 51 | 'skippable_tables' => env('LMG_SQLSRV_SKIPPABLE_TABLES', null), 52 | 'skippable_views' => env('LMG_SQLSRV_SKIPPABLE_VIEWS', null), 53 | ], 54 | ]; 55 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | remote_theme: pmarsceill/just-the-docs 2 | compress_html: 3 | ignore: 4 | envs: all 5 | aux_links: 6 | "View on GitHub": 7 | - "//github.com/bennett-treptow/laravel-migration-generator" 8 | -------------------------------------------------------------------------------- /docs/command.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Running the Generator 4 | nav_order: 3 5 | --- -------------------------------------------------------------------------------- /docs/config.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Config 4 | nav_order: 1 5 | --- -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Laravel Migration Generator 4 | nav_order: 0 5 | --- 6 | # Laravel Migration Generator 7 | 8 | Generate migrations from existing database structures, an alternative to the schema dump provided by Laravel. This package will connect to your database and introspect the schema and generate migration files with columns and indices like they would be if they had originally come from a migration. 9 | 10 | ## Quick Start 11 | ```bash 12 | composer require --dev bennett-treptow/laravel-migration-generator 13 | php artisan vendor:publish --provider="LaravelMigrationGenerator\LaravelMigrationGeneratorProvider" 14 | ``` 15 | 16 | Learn more about [config options](config.md) and [stubs](stubs.md). -------------------------------------------------------------------------------- /docs/stubs.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: default 3 | title: Stubs 4 | nav_order: 2 5 | --- -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | ./tests/Unit 5 | 6 | 7 | -------------------------------------------------------------------------------- /pint.json: -------------------------------------------------------------------------------- 1 | { 2 | "preset": "laravel" 3 | } -------------------------------------------------------------------------------- /src/Commands/GenerateMigrationsCommand.php: -------------------------------------------------------------------------------- 1 | option('connection'); 22 | 23 | if ($connection === 'default') { 24 | $connection = Config::get('database.default'); 25 | } 26 | 27 | if (! Config::has('database.connections.'.$connection)) { 28 | throw new \Exception('Could not find connection `'.$connection.'` in your config.'); 29 | } 30 | 31 | return $connection; 32 | } 33 | 34 | public function getPath($driver) 35 | { 36 | $basePath = $this->option('path'); 37 | if ($basePath === 'default') { 38 | $basePath = ConfigResolver::path($driver); 39 | } 40 | 41 | return $basePath; 42 | } 43 | 44 | public function handle() 45 | { 46 | try { 47 | $connection = $this->getConnection(); 48 | } catch (\Exception $e) { 49 | $this->error($e->getMessage()); 50 | 51 | return 1; 52 | } 53 | 54 | $this->info('Using connection '.$connection); 55 | DB::setDefaultConnection($connection); 56 | 57 | $driver = Config::get('database.connections.'.$connection)['driver']; 58 | 59 | $manager = $this->resolveGeneratorManager($driver); 60 | if ($manager === false) { 61 | $this->error('The `'.$driver.'` driver is not supported at this time.'); 62 | 63 | return 1; 64 | } 65 | 66 | $basePath = base_path($this->getPath($driver)); 67 | 68 | if ($this->option('empty-path') || config('laravel-migration-generator.clear_output_path')) { 69 | foreach (glob($basePath.'/*.php') as $file) { 70 | unlink($file); 71 | } 72 | } 73 | 74 | $this->info('Using '.$basePath.' as the output path..'); 75 | 76 | $tableNames = Arr::wrap($this->option('table')); 77 | 78 | $viewNames = Arr::wrap($this->option('view')); 79 | 80 | $manager->handle($basePath, $tableNames, $viewNames); 81 | } 82 | 83 | /** 84 | * @return false|GeneratorManagerInterface 85 | */ 86 | protected function resolveGeneratorManager(string $driver) 87 | { 88 | $supported = [ 89 | 'mysql' => MySQLGeneratorManager::class, 90 | ]; 91 | 92 | if (! isset($supported[$driver])) { 93 | return false; 94 | } 95 | 96 | return new $supported[$driver](); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/Definitions/ColumnDefinition.php: -------------------------------------------------------------------------------- 1 | $value) { 58 | if (property_exists($this, $attribute)) { 59 | $this->$attribute = $value; 60 | } 61 | } 62 | } 63 | 64 | //region Getters 65 | 66 | public function getMethodName(): string 67 | { 68 | return $this->methodName; 69 | } 70 | 71 | public function getMethodParameters(): array 72 | { 73 | return $this->methodParameters; 74 | } 75 | 76 | public function getColumnName(): ?string 77 | { 78 | return $this->columnName; 79 | } 80 | 81 | public function isUnsigned(): bool 82 | { 83 | return $this->unsigned; 84 | } 85 | 86 | /** 87 | * @return ?bool 88 | */ 89 | public function isNullable(): ?bool 90 | { 91 | return $this->nullable; 92 | } 93 | 94 | /** 95 | * @return mixed 96 | */ 97 | public function getDefaultValue() 98 | { 99 | if (ValueToString::isCastedValue($this->defaultValue)) { 100 | return ValueToString::parseCastedValue($this->defaultValue); 101 | } 102 | 103 | return $this->defaultValue; 104 | } 105 | 106 | public function getComment(): ?string 107 | { 108 | return $this->comment; 109 | } 110 | 111 | public function getCharacterSet(): ?string 112 | { 113 | return $this->characterSet; 114 | } 115 | 116 | public function getCollation(): ?string 117 | { 118 | return $this->collation; 119 | } 120 | 121 | public function isAutoIncrementing(): bool 122 | { 123 | return $this->autoIncrementing; 124 | } 125 | 126 | public function isIndex(): bool 127 | { 128 | return $this->index; 129 | } 130 | 131 | public function isPrimary(): bool 132 | { 133 | return $this->primary; 134 | } 135 | 136 | public function isUnique(): bool 137 | { 138 | return $this->unique; 139 | } 140 | 141 | public function useCurrent(): bool 142 | { 143 | return $this->useCurrent; 144 | } 145 | 146 | public function useCurrentOnUpdate(): bool 147 | { 148 | return $this->useCurrentOnUpdate; 149 | } 150 | 151 | public function getStoredAs(): ?string 152 | { 153 | return $this->storedAs; 154 | } 155 | 156 | public function getVirtualAs(): ?string 157 | { 158 | return $this->virtualAs; 159 | } 160 | 161 | public function isUUID(): bool 162 | { 163 | return $this->isUUID; 164 | } 165 | 166 | //endregion 167 | 168 | //region Setters 169 | 170 | public function setMethodName(string $methodName): ColumnDefinition 171 | { 172 | $this->methodName = $methodName; 173 | 174 | return $this; 175 | } 176 | 177 | public function setMethodParameters(array $methodParameters): ColumnDefinition 178 | { 179 | $this->methodParameters = $methodParameters; 180 | 181 | return $this; 182 | } 183 | 184 | public function setColumnName(?string $columnName): ColumnDefinition 185 | { 186 | $this->columnName = $columnName; 187 | 188 | return $this; 189 | } 190 | 191 | public function setUnsigned(bool $unsigned): ColumnDefinition 192 | { 193 | $this->unsigned = $unsigned; 194 | 195 | return $this; 196 | } 197 | 198 | /** 199 | * @param ?bool $nullable 200 | */ 201 | public function setNullable(?bool $nullable): ColumnDefinition 202 | { 203 | $this->nullable = $nullable; 204 | 205 | return $this; 206 | } 207 | 208 | /** 209 | * @param mixed $defaultValue 210 | * @return ColumnDefinition 211 | */ 212 | public function setDefaultValue($defaultValue) 213 | { 214 | $this->defaultValue = $defaultValue; 215 | 216 | return $this; 217 | } 218 | 219 | public function setComment(?string $comment): ColumnDefinition 220 | { 221 | $this->comment = $comment; 222 | 223 | return $this; 224 | } 225 | 226 | /** 227 | * @param string|null $collation 228 | */ 229 | public function setCharacterSet(?string $characterSet): ColumnDefinition 230 | { 231 | $this->characterSet = $characterSet; 232 | 233 | return $this; 234 | } 235 | 236 | public function setCollation(?string $collation): ColumnDefinition 237 | { 238 | $this->collation = $collation; 239 | 240 | return $this; 241 | } 242 | 243 | public function setAutoIncrementing(bool $autoIncrementing): ColumnDefinition 244 | { 245 | $this->autoIncrementing = $autoIncrementing; 246 | 247 | return $this; 248 | } 249 | 250 | public function setStoredAs(?string $storedAs): ColumnDefinition 251 | { 252 | $this->storedAs = $storedAs; 253 | 254 | return $this; 255 | } 256 | 257 | public function setVirtualAs(?string $virtualAs): ColumnDefinition 258 | { 259 | $this->virtualAs = $virtualAs; 260 | 261 | return $this; 262 | } 263 | 264 | public function addIndexDefinition(IndexDefinition $definition): ColumnDefinition 265 | { 266 | $this->indexDefinitions[] = $definition; 267 | 268 | return $this; 269 | } 270 | 271 | public function setIndex(bool $index): ColumnDefinition 272 | { 273 | $this->index = $index; 274 | 275 | return $this; 276 | } 277 | 278 | public function setPrimary(bool $primary): ColumnDefinition 279 | { 280 | $this->primary = $primary; 281 | 282 | return $this; 283 | } 284 | 285 | public function setUnique(bool $unique): ColumnDefinition 286 | { 287 | $this->unique = $unique; 288 | 289 | return $this; 290 | } 291 | 292 | public function setUseCurrent(bool $useCurrent): ColumnDefinition 293 | { 294 | $this->useCurrent = $useCurrent; 295 | 296 | return $this; 297 | } 298 | 299 | public function setUseCurrentOnUpdate(bool $useCurrentOnUpdate): ColumnDefinition 300 | { 301 | $this->useCurrentOnUpdate = $useCurrentOnUpdate; 302 | 303 | return $this; 304 | } 305 | 306 | public function setIsUUID(bool $isUUID): ColumnDefinition 307 | { 308 | $this->isUUID = $isUUID; 309 | 310 | return $this; 311 | } 312 | 313 | //endregion 314 | 315 | protected function isNullableMethod($methodName) 316 | { 317 | return ! in_array($methodName, ['softDeletes', 'morphs', 'nullableMorphs', 'rememberToken', 'nullableUuidMorphs']) && ! $this->isPrimaryKeyMethod($methodName); 318 | } 319 | 320 | protected function isPrimaryKeyMethod($methodName) 321 | { 322 | return in_array($methodName, ['tinyIncrements', 'mediumIncrements', 'increments', 'bigIncrements', 'id']); 323 | } 324 | 325 | protected function canBeUnsigned($methodName) 326 | { 327 | return ! in_array($methodName, ['morphs', 'nullableMorphs']) && ! $this->isPrimaryKeyMethod($methodName); 328 | } 329 | 330 | protected function guessLaravelMethod() 331 | { 332 | if ($this->primary && $this->unsigned && $this->autoIncrementing) { 333 | //some sort of increments field 334 | if ($this->methodName === 'bigInteger') { 335 | if ($this->columnName === 'id') { 336 | return [null, 'id', []]; 337 | } else { 338 | return [$this->columnName, 'bigIncrements', []]; 339 | } 340 | } elseif ($this->methodName === 'mediumInteger') { 341 | return [$this->columnName, 'mediumIncrements', []]; 342 | } elseif ($this->methodName === 'integer') { 343 | return [$this->columnName, 'increments', []]; 344 | } elseif ($this->methodName === 'smallInteger') { 345 | return [$this->columnName, 'smallIncrements', []]; 346 | } elseif ($this->methodName === 'tinyInteger') { 347 | return [$this->columnName, 'tinyIncrements', []]; 348 | } 349 | } 350 | 351 | if ($this->methodName === 'tinyInteger' && ! $this->unsigned) { 352 | $boolean = false; 353 | if (in_array($this->defaultValue, ['true', 'false', true, false, 'TRUE', 'FALSE', '1', '0', 1, 0], true)) { 354 | $boolean = true; 355 | } 356 | if (Str::startsWith(strtoupper($this->columnName), ['IS_', 'HAS_'])) { 357 | $boolean = true; 358 | } 359 | if ($boolean) { 360 | return [$this->columnName, 'boolean', []]; 361 | } 362 | } 363 | 364 | if ($this->methodName === 'morphs' && $this->nullable === true) { 365 | return [$this->columnName, 'nullableMorphs', []]; 366 | } 367 | 368 | if ($this->methodName === 'uuidMorphs' && $this->nullable === true) { 369 | return [$this->columnName, 'nullableUuidMorphs', []]; 370 | } 371 | 372 | if ($this->methodName === 'string' && $this->columnName === 'remember_token' && $this->nullable === true) { 373 | return [null, 'rememberToken', []]; 374 | } 375 | if ($this->isUUID() && $this->methodName !== 'uuidMorphs') { 376 | //only override if not already uuidMorphs 377 | return [$this->columnName, 'uuid', []]; 378 | } 379 | 380 | if (config('laravel-migration-generator.definitions.prefer_unsigned_prefix') && $this->unsigned) { 381 | $availableUnsignedPrefixes = [ 382 | 'bigInteger', 383 | 'decimal', 384 | 'integer', 385 | 'mediumInteger', 386 | 'smallInteger', 387 | 'tinyInteger', 388 | ]; 389 | if (in_array($this->methodName, $availableUnsignedPrefixes)) { 390 | return [$this->columnName, 'unsigned'.ucfirst($this->methodName), $this->methodParameters]; 391 | } 392 | } 393 | 394 | return [$this->columnName, $this->methodName, $this->methodParameters]; 395 | } 396 | 397 | public function render(): string 398 | { 399 | [$finalColumnName, $finalMethodName, $finalMethodParameters] = $this->guessLaravelMethod(); 400 | 401 | $initialString = '$table->'.$finalMethodName.'('; 402 | if ($finalColumnName !== null) { 403 | $initialString .= ValueToString::make($finalColumnName); 404 | } 405 | if (count($finalMethodParameters) > 0) { 406 | foreach ($finalMethodParameters as $param) { 407 | $initialString .= ', '.ValueToString::make($param); 408 | } 409 | } 410 | $initialString .= ')'; 411 | if ($this->unsigned && $this->canBeUnsigned($finalMethodName) && ! Str::startsWith($finalMethodName, 'unsigned')) { 412 | $initialString .= '->unsigned()'; 413 | } 414 | 415 | if ($this->defaultValue === 'NULL') { 416 | $this->defaultValue = null; 417 | $this->nullable = true; 418 | } 419 | 420 | if ($this->isNullableMethod($finalMethodName)) { 421 | if ($this->nullable === true) { 422 | $initialString .= '->nullable()'; 423 | } 424 | } 425 | 426 | if ($this->defaultValue !== null) { 427 | $initialString .= '->default('; 428 | $initialString .= ValueToString::make($this->defaultValue, false); 429 | $initialString .= ')'; 430 | } 431 | if ($this->useCurrent) { 432 | $initialString .= '->useCurrent()'; 433 | } 434 | if ($this->useCurrentOnUpdate) { 435 | $initialString .= '->useCurrentOnUpdate()'; 436 | } 437 | 438 | if ($this->index) { 439 | $indexName = ''; 440 | if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_index_names')) { 441 | $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); 442 | } 443 | $initialString .= '->index('.$indexName.')'; 444 | } 445 | 446 | if ($this->primary && ! $this->isPrimaryKeyMethod($finalMethodName)) { 447 | $indexName = ''; 448 | if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_primary_key_index_names')) { 449 | if ($this->indexDefinitions[0]->getIndexName() !== null) { 450 | $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); 451 | } 452 | } 453 | $initialString .= '->primary('.$indexName.')'; 454 | } 455 | 456 | if ($this->unique) { 457 | $indexName = ''; 458 | if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_unique_key_index_names')) { 459 | $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); 460 | } 461 | $initialString .= '->unique('.$indexName.')'; 462 | } 463 | 464 | if ($this->storedAs !== null) { 465 | $initialString .= '->storedAs('.ValueToString::make(str_replace('"', '\"', $this->storedAs), false, false).')'; 466 | } 467 | 468 | if ($this->virtualAs !== null) { 469 | $initialString .= '->virtualAs('.ValueToString::make(str_replace('"', '\"', $this->virtualAs), false, false).')'; 470 | } 471 | 472 | if ($this->comment !== null && config('laravel-migration-generator.definitions.with_comments')) { 473 | $initialString .= '->comment('.ValueToString::make(str_replace('"', '\"', $this->comment), false, false).')'; 474 | } 475 | 476 | return $initialString; 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /src/Definitions/IndexDefinition.php: -------------------------------------------------------------------------------- 1 | $value) { 29 | if (property_exists($this, $attribute)) { 30 | $this->$attribute = $value; 31 | } 32 | } 33 | } 34 | 35 | //region Getters 36 | 37 | public function getIndexType(): string 38 | { 39 | return $this->indexType; 40 | } 41 | 42 | public function getIndexName(): ?string 43 | { 44 | return $this->indexName; 45 | } 46 | 47 | public function getIndexColumns(): array 48 | { 49 | return $this->indexColumns; 50 | } 51 | 52 | public function getForeignReferencedColumns(): array 53 | { 54 | return $this->foreignReferencedColumns; 55 | } 56 | 57 | public function getForeignReferencedTable(): string 58 | { 59 | return $this->foreignReferencedTable; 60 | } 61 | 62 | public function getConstraintActions(): array 63 | { 64 | return $this->constraintActions; 65 | } 66 | 67 | //endregion 68 | //region Setters 69 | 70 | public function setIndexType(string $indexType): IndexDefinition 71 | { 72 | $this->indexType = $indexType; 73 | 74 | return $this; 75 | } 76 | 77 | public function setIndexName(string $indexName): IndexDefinition 78 | { 79 | $this->indexName = $indexName; 80 | 81 | return $this; 82 | } 83 | 84 | public function setIndexColumns(array $indexColumns): IndexDefinition 85 | { 86 | $this->indexColumns = $indexColumns; 87 | 88 | return $this; 89 | } 90 | 91 | public function setForeignReferencedColumns(array $foreignReferencedColumns): IndexDefinition 92 | { 93 | $this->foreignReferencedColumns = $foreignReferencedColumns; 94 | 95 | return $this; 96 | } 97 | 98 | public function setForeignReferencedTable(string $foreignReferencedTable): IndexDefinition 99 | { 100 | $this->foreignReferencedTable = $foreignReferencedTable; 101 | 102 | return $this; 103 | } 104 | 105 | public function setConstraintActions(array $constraintActions): IndexDefinition 106 | { 107 | $this->constraintActions = $constraintActions; 108 | 109 | return $this; 110 | } 111 | 112 | //endregion 113 | 114 | public function isMultiColumnIndex() 115 | { 116 | return count($this->indexColumns) > 1; 117 | } 118 | 119 | public function render(): string 120 | { 121 | if ($this->indexType === 'foreign') { 122 | $indexName = ''; 123 | if (config('laravel-migration-generator.definitions.use_defined_foreign_key_index_names')) { 124 | $indexName = ', \''.$this->getIndexName().'\''; 125 | } 126 | 127 | $base = '$table->foreign('.ValueToString::make($this->indexColumns, true).$indexName.')->references('.ValueToString::make($this->foreignReferencedColumns, true).')->on('.ValueToString::make($this->foreignReferencedTable).')'; 128 | foreach ($this->constraintActions as $type => $action) { 129 | $base .= '->on'.ucfirst($type).'('.ValueToString::make($action).')'; 130 | } 131 | 132 | return $base; 133 | } elseif ($this->indexType === 'primary') { 134 | $indexName = ''; 135 | if (config('laravel-migration-generator.definitions.use_defined_primary_key_index_names') && $this->getIndexName() !== null) { 136 | $indexName = ', \''.$this->getIndexName().'\''; 137 | } 138 | 139 | return '$table->primary('.ValueToString::make($this->indexColumns).$indexName.')'; 140 | } elseif ($this->indexType === 'unique') { 141 | $indexName = ''; 142 | if (config('laravel-migration-generator.definitions.use_defined_unique_key_index_names')) { 143 | $indexName = ', \''.$this->getIndexName().'\''; 144 | } 145 | 146 | return '$table->unique('.ValueToString::make($this->indexColumns).$indexName.')'; 147 | } elseif ($this->indexType === 'index') { 148 | $indexName = ''; 149 | if (config('laravel-migration-generator.definitions.use_defined_index_names')) { 150 | $indexName = ', \''.$this->getIndexName().'\''; 151 | } 152 | 153 | return '$table->index('.ValueToString::make($this->indexColumns).$indexName.')'; 154 | } 155 | 156 | return ''; 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/Definitions/TableDefinition.php: -------------------------------------------------------------------------------- 1 | */ 14 | protected array $columnDefinitions = []; 15 | 16 | protected array $indexDefinitions = []; 17 | 18 | public function __construct($attributes = []) 19 | { 20 | foreach ($attributes as $attribute => $value) { 21 | if (property_exists($this, $attribute)) { 22 | $this->$attribute = $value; 23 | } 24 | } 25 | } 26 | 27 | public function getDriver(): string 28 | { 29 | return $this->driver; 30 | } 31 | 32 | public function getPresentableTableName(): string 33 | { 34 | if (count($this->getColumnDefinitions()) === 0) { 35 | if (count($definitions = $this->getIndexDefinitions()) > 0) { 36 | $first = collect($definitions)->first(); 37 | 38 | //a fk only table from dependency resolution 39 | return $this->getTableName().'_'.$first->getIndexName(); 40 | } 41 | } 42 | 43 | return $this->getTableName(); 44 | } 45 | 46 | public function getTableName(): string 47 | { 48 | return $this->tableName; 49 | } 50 | 51 | public function setTableName(string $tableName) 52 | { 53 | $this->tableName = $tableName; 54 | 55 | return $this; 56 | } 57 | 58 | public function getColumnDefinitions(): array 59 | { 60 | return $this->columnDefinitions; 61 | } 62 | 63 | public function setColumnDefinitions(array $columnDefinitions) 64 | { 65 | $this->columnDefinitions = $columnDefinitions; 66 | 67 | return $this; 68 | } 69 | 70 | public function addColumnDefinition(ColumnDefinition $definition) 71 | { 72 | $this->columnDefinitions[] = $definition; 73 | 74 | return $this; 75 | } 76 | 77 | /** 78 | * @return array 79 | */ 80 | public function getIndexDefinitions(): array 81 | { 82 | return $this->indexDefinitions; 83 | } 84 | 85 | /** @return array */ 86 | public function getForeignKeyDefinitions(): array 87 | { 88 | return collect($this->getIndexDefinitions())->filter(function ($indexDefinition) { 89 | return $indexDefinition->getIndexType() == IndexDefinition::TYPE_FOREIGN; 90 | })->toArray(); 91 | } 92 | 93 | public function setIndexDefinitions(array $indexDefinitions) 94 | { 95 | $this->indexDefinitions = $indexDefinitions; 96 | 97 | return $this; 98 | } 99 | 100 | public function addIndexDefinition(IndexDefinition $definition) 101 | { 102 | $this->indexDefinitions[] = $definition; 103 | 104 | return $this; 105 | } 106 | 107 | public function removeIndexDefinition(IndexDefinition $definition) 108 | { 109 | foreach ($this->indexDefinitions as $key => $indexDefinition) { 110 | if ($definition->getIndexName() == $indexDefinition->getIndexName()) { 111 | unset($this->indexDefinitions[$key]); 112 | 113 | break; 114 | } 115 | } 116 | 117 | return $this; 118 | } 119 | 120 | public function getPrimaryKey(): array 121 | { 122 | return collect($this->getColumnDefinitions()) 123 | ->filter(function (ColumnDefinition $columnDefinition) { 124 | return $columnDefinition->isPrimary(); 125 | })->toArray(); 126 | } 127 | 128 | public function formatter(): TableFormatter 129 | { 130 | return new TableFormatter($this); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/Definitions/ViewDefinition.php: -------------------------------------------------------------------------------- 1 | $value) { 18 | if (property_exists($this, $attribute)) { 19 | $this->$attribute = $value; 20 | } 21 | } 22 | } 23 | 24 | public function getDriver(): string 25 | { 26 | return $this->driver; 27 | } 28 | 29 | public function setDriver(string $driver): ViewDefinition 30 | { 31 | $this->driver = $driver; 32 | 33 | return $this; 34 | } 35 | 36 | public function getSchema(): string 37 | { 38 | return $this->schema; 39 | } 40 | 41 | public function setSchema(string $schema): ViewDefinition 42 | { 43 | $this->schema = $schema; 44 | 45 | return $this; 46 | } 47 | 48 | public function getViewName(): string 49 | { 50 | return $this->viewName; 51 | } 52 | 53 | public function setViewName(string $viewName): ViewDefinition 54 | { 55 | $this->viewName = $viewName; 56 | 57 | return $this; 58 | } 59 | 60 | public function formatter(): ViewFormatter 61 | { 62 | return new ViewFormatter($this); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Formatters/TableFormatter.php: -------------------------------------------------------------------------------- 1 | tableDefinition = $tableDefinition; 17 | } 18 | 19 | public function render($tabCharacter = ' ') 20 | { 21 | $tableName = $this->tableDefinition->getPresentableTableName(); 22 | 23 | $schema = $this->getSchema($tabCharacter); 24 | $stub = file_get_contents($this->getStubPath()); 25 | if (strpos($stub, '[TableUp]') !== false) { 26 | //uses new syntax 27 | $stub = Formatter::replace($tabCharacter, '[TableUp]', $this->stubTableUp($tabCharacter), $stub); 28 | $stub = Formatter::replace($tabCharacter, '[TableDown]', $this->stubTableDown($tabCharacter), $stub); 29 | } 30 | 31 | $stub = str_replace('[TableName:Studly]', Str::studly($tableName), $stub); 32 | $stub = str_replace('[TableName]', $tableName, $stub); 33 | $stub = Formatter::replace($tabCharacter, '[Schema]', $schema, $stub); 34 | 35 | return $stub; 36 | } 37 | 38 | public function getStubFileName($index = 0): string 39 | { 40 | $driver = $this->tableDefinition->getDriver(); 41 | $baseStubFileName = ConfigResolver::tableNamingScheme($driver); 42 | foreach ($this->stubNameVariables($index) as $variable => $replacement) { 43 | if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { 44 | $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); 45 | } 46 | } 47 | 48 | return $baseStubFileName; 49 | } 50 | 51 | public function getStubPath(): string 52 | { 53 | $driver = $this->tableDefinition->getDriver(); 54 | 55 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table.stub'))) { 56 | return $overridden; 57 | } 58 | 59 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table.stub'))) { 60 | return $overridden; 61 | } 62 | 63 | return __DIR__.'/../../stubs/table.stub'; 64 | } 65 | 66 | public function getStubCreatePath(): string 67 | { 68 | $driver = $this->tableDefinition->getDriver(); 69 | 70 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table-create.stub'))) { 71 | return $overridden; 72 | } 73 | 74 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table-create.stub'))) { 75 | return $overridden; 76 | } 77 | 78 | return __DIR__.'/../../stubs/table-create.stub'; 79 | } 80 | 81 | public function getStubModifyPath(): string 82 | { 83 | $driver = $this->tableDefinition->getDriver(); 84 | 85 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table-modify.stub'))) { 86 | return $overridden; 87 | } 88 | 89 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table-modify.stub'))) { 90 | return $overridden; 91 | } 92 | 93 | return __DIR__.'/../../stubs/table-modify.stub'; 94 | } 95 | 96 | public function stubNameVariables($index): array 97 | { 98 | $tableName = $this->tableDefinition->getPresentableTableName(); 99 | 100 | return [ 101 | 'TableName:Studly' => Str::studly($tableName), 102 | 'TableName:Lowercase' => strtolower($tableName), 103 | 'TableName' => $tableName, 104 | 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), 105 | 'Index' => (string) $index, 106 | 'IndexedEmptyTimestamp' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 107 | 'IndexedTimestamp' => app('laravel-migration-generator:time')->clone()->addSeconds($index)->format('Y_m_d_His'), 108 | ]; 109 | } 110 | 111 | public function getSchema($tab = ''): string 112 | { 113 | $formatter = new Formatter($tab); 114 | collect($this->tableDefinition->getColumnDefinitions()) 115 | ->filter(fn ($col) => $col->isWritable()) 116 | ->each(function ($column) use ($formatter) { 117 | $formatter->line($column->render().';'); 118 | }); 119 | 120 | $indices = collect($this->tableDefinition->getIndexDefinitions()) 121 | ->filter(fn ($index) => $index->isWritable()); 122 | 123 | if ($indices->count() > 0) { 124 | if (count($this->tableDefinition->getColumnDefinitions()) > 0) { 125 | $formatter->line(''); 126 | } 127 | $indices->each(function ($index) use ($formatter) { 128 | $formatter->line($index->render().';'); 129 | }); 130 | } 131 | 132 | return $formatter->render(); 133 | } 134 | 135 | public function stubTableUp($tab = '', $variables = null): string 136 | { 137 | if ($variables === null) { 138 | $variables = $this->getStubVariables($tab); 139 | } 140 | if (count($this->tableDefinition->getColumnDefinitions()) === 0) { 141 | $tableModifyStub = file_get_contents($this->getStubModifyPath()); 142 | foreach ($variables as $var => $replacement) { 143 | $tableModifyStub = Formatter::replace($tab, '['.$var.']', $replacement, $tableModifyStub); 144 | } 145 | 146 | return $tableModifyStub; 147 | } 148 | 149 | $tableUpStub = file_get_contents($this->getStubCreatePath()); 150 | foreach ($variables as $var => $replacement) { 151 | $tableUpStub = Formatter::replace($tab, '['.$var.']', $replacement, $tableUpStub); 152 | } 153 | 154 | return $tableUpStub; 155 | } 156 | 157 | public function stubTableDown($tab = ''): string 158 | { 159 | if (count($this->tableDefinition->getColumnDefinitions()) === 0) { 160 | $schema = 'Schema::table(\''.$this->tableDefinition->getTableName().'\', function(Blueprint $table){'."\n"; 161 | foreach ($this->tableDefinition->getForeignKeyDefinitions() as $indexDefinition) { 162 | $schema .= $tab.'$table->dropForeign(\''.$indexDefinition->getIndexName().'\');'."\n"; 163 | } 164 | 165 | return $schema.'});'; 166 | } 167 | 168 | return 'Schema::dropIfExists(\''.$this->tableDefinition->getTableName().'\');'; 169 | } 170 | 171 | protected function getStubVariables($tab = '') 172 | { 173 | $tableName = $this->tableDefinition->getTableName(); 174 | 175 | return [ 176 | 'TableName:Studly' => Str::studly($tableName), 177 | 'TableName:Lowercase' => strtolower($tableName), 178 | 'TableName' => $tableName, 179 | 'Schema' => $this->getSchema($tab), 180 | ]; 181 | } 182 | 183 | public function write(string $basePath, $index = 0, string $tabCharacter = ' '): string 184 | { 185 | $stub = $this->render($tabCharacter); 186 | 187 | $fileName = $this->getStubFileName($index); 188 | file_put_contents($final = $basePath.'/'.$fileName, $stub); 189 | 190 | return $final; 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /src/Formatters/ViewFormatter.php: -------------------------------------------------------------------------------- 1 | definition = $definition; 17 | } 18 | 19 | public function stubNameVariables($index = 0) 20 | { 21 | return [ 22 | 'ViewName:Studly' => Str::studly($viewName = $this->definition->getViewName()), 23 | 'ViewName:Lowercase' => strtolower($viewName), 24 | 'ViewName' => $viewName, 25 | 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), 26 | 'Index' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 27 | 'IndexedEmptyTimestamp' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 28 | 'IndexedTimestamp' => app('laravel-migration-generator:time')->clone()->addSeconds($index)->format('Y_m_d_His'), 29 | ]; 30 | } 31 | 32 | protected function getStubFileName($index = 0) 33 | { 34 | $driver = $this->definition->getDriver(); 35 | 36 | $baseStubFileName = ConfigResolver::viewNamingScheme($driver); 37 | foreach ($this->stubNameVariables($index) as $variable => $replacement) { 38 | if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { 39 | $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); 40 | } 41 | } 42 | 43 | return $baseStubFileName; 44 | } 45 | 46 | protected function getStubPath() 47 | { 48 | $driver = $this->definition->getDriver(); 49 | 50 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-view.stub'))) { 51 | return $overridden; 52 | } 53 | 54 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/view.stub'))) { 55 | return $overridden; 56 | } 57 | 58 | return __DIR__.'/../../stubs/view.stub'; 59 | } 60 | 61 | public function render($tabCharacter = ' ') 62 | { 63 | $schema = $this->definition->getSchema(); 64 | $stub = file_get_contents($this->getStubPath()); 65 | $variables = [ 66 | '[ViewName:Studly]' => Str::studly($viewName = $this->definition->getViewName()), 67 | '[ViewName]' => $viewName, 68 | '[Schema]' => $schema, 69 | ]; 70 | foreach ($variables as $key => $value) { 71 | $stub = Formatter::replace($tabCharacter, $key, $value, $stub); 72 | } 73 | 74 | return $stub; 75 | } 76 | 77 | public function write(string $basePath, $index = 0, string $tabCharacter = ' '): string 78 | { 79 | $stub = $this->render($tabCharacter); 80 | 81 | $fileName = $this->getStubFileName($index); 82 | file_put_contents($final = $basePath.'/'.$fileName, $stub); 83 | 84 | return $final; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/GeneratorManagers/BaseGeneratorManager.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | public function getTableDefinitions(): array 30 | { 31 | return $this->tableDefinitions; 32 | } 33 | 34 | /** 35 | * @return array 36 | */ 37 | public function getViewDefinitions(): array 38 | { 39 | return $this->viewDefinitions; 40 | } 41 | 42 | public function addTableDefinition(TableDefinition $tableDefinition): BaseGeneratorManager 43 | { 44 | $this->tableDefinitions[] = $tableDefinition; 45 | 46 | return $this; 47 | } 48 | 49 | public function addViewDefinition(ViewDefinition $definition): BaseGeneratorManager 50 | { 51 | $this->viewDefinitions[] = $definition; 52 | 53 | return $this; 54 | } 55 | 56 | public function handle(string $basePath, array $tableNames = [], array $viewNames = []) 57 | { 58 | $this->init(); 59 | 60 | $tableDefinitions = collect($this->getTableDefinitions()); 61 | $viewDefinitions = collect($this->getViewDefinitions()); 62 | 63 | $this->createMissingDirectory($basePath); 64 | 65 | if (count($tableNames) > 0) { 66 | $tableDefinitions = $tableDefinitions->filter(function ($tableDefinition) use ($tableNames) { 67 | return in_array($tableDefinition->getTableName(), $tableNames); 68 | }); 69 | } 70 | if (count($viewNames) > 0) { 71 | $viewDefinitions = $viewDefinitions->filter(function ($viewGenerator) use ($viewNames) { 72 | return in_array($viewGenerator->getViewName(), $viewNames); 73 | }); 74 | } 75 | 76 | $tableDefinitions = $tableDefinitions->filter(function ($tableDefinition) { 77 | return ! $this->skipTable($tableDefinition->getTableName()); 78 | }); 79 | 80 | $viewDefinitions = $viewDefinitions->filter(function ($viewDefinition) { 81 | return ! $this->skipView($viewDefinition->getViewName()); 82 | }); 83 | 84 | $sorted = $this->sortTables($tableDefinitions->toArray()); 85 | 86 | $this->writeTableMigrations($sorted, $basePath); 87 | 88 | $this->writeViewMigrations($viewDefinitions->toArray(), $basePath, count($sorted)); 89 | } 90 | 91 | /** 92 | * @param array $tableDefinitions 93 | * @return array 94 | */ 95 | public function sortTables(array $tableDefinitions): array 96 | { 97 | if (count($tableDefinitions) <= 1) { 98 | return $tableDefinitions; 99 | } 100 | 101 | if (config('laravel-migration-generator.sort_mode') == 'foreign_key') { 102 | return (new DependencyResolver($tableDefinitions))->getDependencyOrder(); 103 | } 104 | 105 | return $tableDefinitions; 106 | } 107 | 108 | /** 109 | * @param array $tableDefinitions 110 | */ 111 | public function writeTableMigrations(array $tableDefinitions, $basePath) 112 | { 113 | foreach ($tableDefinitions as $key => $tableDefinition) { 114 | $tableDefinition->formatter()->write($basePath, $key); 115 | } 116 | } 117 | 118 | /** 119 | * @param array $viewDefinitions 120 | */ 121 | public function writeViewMigrations(array $viewDefinitions, $basePath, $tableCount = 0) 122 | { 123 | foreach ($viewDefinitions as $key => $view) { 124 | $view->formatter()->write($basePath, $tableCount + $key); 125 | } 126 | } 127 | 128 | /** 129 | * @return array 130 | */ 131 | public function skippableTables(): array 132 | { 133 | return ConfigResolver::skippableTables(static::driver()); 134 | } 135 | 136 | public function skipTable($table): bool 137 | { 138 | return in_array($table, $this->skippableTables()); 139 | } 140 | 141 | /** 142 | * @return array 143 | */ 144 | public function skippableViews(): array 145 | { 146 | return ConfigResolver::skippableViews(static::driver()); 147 | } 148 | 149 | public function skipView($view): bool 150 | { 151 | $skipViews = config('laravel-migration-generator.skip_views'); 152 | if ($skipViews) { 153 | return true; 154 | } 155 | 156 | return in_array($view, $this->skippableViews()); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /src/GeneratorManagers/Interfaces/GeneratorManagerInterface.php: -------------------------------------------------------------------------------- 1 | $table) { 24 | $tableData = (array) $table; 25 | $table = $tableData[array_key_first($tableData)]; 26 | $tableType = $tableData['Table_type']; 27 | if ($tableType === 'BASE TABLE') { 28 | $this->addTableDefinition(TableGenerator::init($table)->definition()); 29 | } elseif ($tableType === 'VIEW') { 30 | $this->addViewDefinition(ViewGenerator::init($table)->definition()); 31 | } 32 | } 33 | } 34 | 35 | public function addTableDefinition(TableDefinition $tableDefinition): BaseGeneratorManager 36 | { 37 | $prefix = config('database.connections.'.DB::getDefaultConnection().'.prefix', ''); 38 | if (! empty($prefix) && Str::startsWith($tableDefinition->getTableName(), $prefix)) { 39 | $tableDefinition->setTableName(Str::replaceFirst($prefix, '', $tableDefinition->getTableName())); 40 | } 41 | 42 | return parent::addTableDefinition($tableDefinition); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Generators/BaseTableGenerator.php: -------------------------------------------------------------------------------- 1 | definition = new TableDefinition([ 26 | 'driver' => static::driver(), 27 | 'tableName' => $tableName, 28 | ]); 29 | $this->rows = $rows; 30 | } 31 | 32 | public function definition(): TableDefinition 33 | { 34 | return $this->definition; 35 | } 36 | 37 | abstract public function resolveStructure(); 38 | 39 | abstract public function parse(); 40 | 41 | public static function init(string $tableName, array $rows = []) 42 | { 43 | $instance = (new static($tableName, $rows)); 44 | 45 | if ($instance->shouldResolveStructure()) { 46 | $instance->resolveStructure(); 47 | } 48 | 49 | $instance->parse(); 50 | $instance->cleanUp(); 51 | 52 | return $instance; 53 | } 54 | 55 | public function shouldResolveStructure(): bool 56 | { 57 | return count($this->rows) === 0; 58 | } 59 | 60 | public function cleanUp(): void 61 | { 62 | $this->cleanUpForeignKeyIndices(); 63 | 64 | $this->cleanUpMorphColumns(); 65 | 66 | if (! config('laravel-migration-generator.definitions.use_defined_datatype_on_timestamp')) { 67 | $this->cleanUpTimestampsColumn(); 68 | } 69 | 70 | $this->cleanUpColumnsWithIndices(); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Generators/BaseViewGenerator.php: -------------------------------------------------------------------------------- 1 | definition = new ViewDefinition([ 15 | 'driver' => static::driver(), 16 | 'viewName' => $viewName, 17 | 'schema' => $schema, 18 | ]); 19 | } 20 | 21 | public function definition(): ViewDefinition 22 | { 23 | return $this->definition; 24 | } 25 | 26 | public static function init(string $viewName, ?string $schema = null) 27 | { 28 | $obj = new static($viewName, $schema); 29 | if ($schema === null) { 30 | $obj->resolveSchema(); 31 | } 32 | $obj->parse(); 33 | 34 | return $obj; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Generators/Concerns/CleansUpColumnIndices.php: -------------------------------------------------------------------------------- 1 | definition()->getIndexDefinitions() as &$index) { 17 | /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $index */ 18 | if (! $index->isWritable()) { 19 | continue; 20 | } 21 | $columns = $index->getIndexColumns(); 22 | 23 | foreach ($columns as $indexColumn) { 24 | foreach ($this->definition()->getColumnDefinitions() as $column) { 25 | if ($column->getColumnName() === $indexColumn) { 26 | $indexType = $index->getIndexType(); 27 | $isMultiColumnIndex = $index->isMultiColumnIndex(); 28 | 29 | if ($indexType === 'primary' && ! $isMultiColumnIndex) { 30 | $column->setPrimary(true)->addIndexDefinition($index); 31 | $index->markAsWritable(false); 32 | } elseif ($indexType === 'index' && ! $isMultiColumnIndex) { 33 | $isForeignKeyIndex = false; 34 | foreach ($this->definition()->getIndexDefinitions() as $innerIndex) { 35 | if ($innerIndex->getIndexType() === 'foreign' && ! $innerIndex->isMultiColumnIndex() && $innerIndex->getIndexColumns()[0] == $column->getColumnName()) { 36 | $isForeignKeyIndex = true; 37 | 38 | break; 39 | } 40 | } 41 | if ($isForeignKeyIndex === false) { 42 | $column->setIndex(true)->addIndexDefinition($index); 43 | } 44 | $index->markAsWritable(false); 45 | } elseif ($indexType === 'unique' && ! $isMultiColumnIndex) { 46 | $column->setUnique(true)->addIndexDefinition($index); 47 | $index->markAsWritable(false); 48 | } 49 | } 50 | } 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Generators/Concerns/CleansUpForeignKeyIndices.php: -------------------------------------------------------------------------------- 1 | definition()->getIndexDefinitions(); 17 | foreach ($indexDefinitions as $index) { 18 | /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $index */ 19 | if ($index->getIndexType() === 'index') { 20 | //look for corresponding foreign key for this index 21 | $columns = $index->getIndexColumns(); 22 | $indexName = $index->getIndexName(); 23 | 24 | foreach ($indexDefinitions as $innerIndex) { 25 | /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $innerIndex */ 26 | if ($innerIndex->getIndexName() !== $indexName) { 27 | if ($innerIndex->getIndexType() === 'foreign') { 28 | $cols = $innerIndex->getIndexColumns(); 29 | if (count(array_intersect($columns, $cols)) === count($columns)) { 30 | //has same columns 31 | $index->markAsWritable(false); 32 | 33 | break; 34 | } 35 | } 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Generators/Concerns/CleansUpMorphColumns.php: -------------------------------------------------------------------------------- 1 | definition()->getColumnDefinitions() as &$column) { 21 | if (Str::endsWith($columnName = $column->getColumnName(), ['_id', '_type'])) { 22 | $pieces = explode('_', $columnName); 23 | $type = array_pop($pieces); //pop off id or type 24 | $morphColumn = implode('_', $pieces); 25 | $morphColumns[$morphColumn][$type] = $column; 26 | } 27 | } 28 | 29 | foreach ($morphColumns as $columnName => $fields) { 30 | if (count($fields) === 2) { 31 | /** @var ColumnDefinition $idField */ 32 | $idField = $fields['id']; 33 | /** @var ColumnDefinition $typeField */ 34 | $typeField = $fields['type']; 35 | 36 | if (! ($idField->isUUID() || Str::contains($idField->getMethodName(), 'integer'))) { 37 | //should only be a uuid field or integer 38 | continue; 39 | } 40 | if ($typeField->getMethodName() != 'string') { 41 | //should only be a string field 42 | continue; 43 | } 44 | 45 | if ($idField->isUUID()) { 46 | //UUID morph 47 | $idField 48 | ->setMethodName('uuidMorphs') 49 | ->setMethodParameters([]) 50 | ->setColumnName($columnName); 51 | } else { 52 | //regular morph 53 | $idField 54 | ->setMethodName('morphs') 55 | ->setColumnName($columnName); 56 | } 57 | $typeField->markAsWritable(false); 58 | 59 | foreach ($this->definition->getIndexDefinitions() as $index) { 60 | $columns = $index->getIndexColumns(); 61 | $morphColumns = [$columnName.'_id', $columnName.'_type']; 62 | 63 | if (count($columns) == count($morphColumns) && array_diff($columns, $morphColumns) === array_diff($morphColumns, $columns)) { 64 | $index->markAsWritable(false); 65 | 66 | break; 67 | } 68 | } 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Generators/Concerns/CleansUpTimestampsColumn.php: -------------------------------------------------------------------------------- 1 | definition()->getColumnDefinitions() as &$column) { 18 | $columnName = $column->getColumnName(); 19 | if ($columnName === 'created_at') { 20 | $timestampColumns['created_at'] = $column; 21 | } elseif ($columnName === 'updated_at') { 22 | $timestampColumns['updated_at'] = $column; 23 | } 24 | if (count($timestampColumns) === 2) { 25 | foreach ($timestampColumns as $timestampColumn) { 26 | if ($timestampColumn->useCurrent() || $timestampColumn->useCurrentOnUpdate()) { 27 | //don't convert to a `timestamps()` method if useCurrent is used 28 | 29 | return; 30 | } 31 | } 32 | $timestampColumns['created_at'] 33 | ->setColumnName(null) 34 | ->setMethodName('timestamps') 35 | ->setNullable(false); 36 | $timestampColumns['updated_at']->markAsWritable(false); 37 | 38 | break; 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Generators/Concerns/WritesToFile.php: -------------------------------------------------------------------------------- 1 | isWritable()) { 10 | return; 11 | } 12 | 13 | $stub = $this->generateStub($tabCharacter); 14 | 15 | $fileName = $this->getStubFileName($index); 16 | file_put_contents($basePath.'/'.$fileName, $stub); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Generators/Concerns/WritesViewsToFile.php: -------------------------------------------------------------------------------- 1 | Str::studly($this->viewName), 16 | 'ViewName:Lowercase' => strtolower($this->viewName), 17 | 'ViewName' => $this->viewName, 18 | 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), 19 | ]; 20 | } 21 | 22 | protected function getStubFileName() 23 | { 24 | $driver = static::driver(); 25 | 26 | $baseStubFileName = ConfigResolver::viewNamingScheme($driver); 27 | foreach ($this->stubNameVariables() as $variable => $replacement) { 28 | if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { 29 | $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); 30 | } 31 | } 32 | 33 | return $baseStubFileName; 34 | } 35 | 36 | protected function getStubPath() 37 | { 38 | $driver = static::driver(); 39 | 40 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-view.stub'))) { 41 | return $overridden; 42 | } 43 | 44 | if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/view.stub'))) { 45 | return $overridden; 46 | } 47 | 48 | return __DIR__.'/../../../stubs/view.stub'; 49 | } 50 | 51 | protected function generateStub($tabCharacter = ' ') 52 | { 53 | $tab = str_repeat($tabCharacter, 3); 54 | 55 | $schema = $this->getSchema(); 56 | $stub = file_get_contents($this->getStubPath()); 57 | $stub = str_replace('[ViewName:Studly]', Str::studly($this->viewName), $stub); 58 | $stub = str_replace('[ViewName]', $this->viewName, $stub); 59 | $stub = str_replace('[Schema]', $tab.$schema, $stub); 60 | 61 | return $stub; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Generators/Interfaces/TableGeneratorInterface.php: -------------------------------------------------------------------------------- 1 | definition()->getTableName().'`'); 24 | $structure = $structure[0]; 25 | $structure = (array) $structure; 26 | if (isset($structure['Create Table'])) { 27 | $lines = explode("\n", $structure['Create Table']); 28 | 29 | array_shift($lines); //get rid of first line 30 | array_pop($lines); //get rid of last line 31 | 32 | $lines = array_map(fn ($item) => trim($item), $lines); 33 | $this->rows = $lines; 34 | } else { 35 | $this->rows = []; 36 | } 37 | } 38 | 39 | protected function isColumnLine($line) 40 | { 41 | return ! Str::startsWith($line, ['KEY', 'PRIMARY', 'UNIQUE', 'FULLTEXT', 'CONSTRAINT']); 42 | } 43 | 44 | public function parse() 45 | { 46 | foreach ($this->rows as $line) { 47 | if ($this->isColumnLine($line)) { 48 | $tokenizer = ColumnTokenizer::parse($line); 49 | $this->definition()->addColumnDefinition($tokenizer->definition()); 50 | } else { 51 | $tokenizer = IndexTokenizer::parse($line); 52 | $this->definition()->addIndexDefinition($tokenizer->definition()); 53 | } 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Generators/MySQL/ViewGenerator.php: -------------------------------------------------------------------------------- 1 | definition()->getViewName().'`'); 19 | $structure = $structure[0]; 20 | $structure = (array) $structure; 21 | if (isset($structure['Create View'])) { 22 | $this->definition()->setSchema($structure['Create View']); 23 | } 24 | } 25 | 26 | public function parse() 27 | { 28 | $schema = $this->definition()->getSchema(); 29 | if (preg_match('/CREATE(.*?)VIEW/', $schema, $matches)) { 30 | $schema = str_replace($matches[1], ' ', $schema); 31 | } 32 | 33 | if (preg_match_all('/isnull\((.+?)\)/', $schema, $matches)) { 34 | foreach ($matches[0] as $key => $match) { 35 | $schema = str_replace($match, $matches[1][$key].' IS NULL', $schema); 36 | } 37 | } 38 | if (preg_match('/collate utf8mb4_unicode_ci/', $schema)) { 39 | $schema = str_replace('collate utf8mb4_unicode_ci', '', $schema); 40 | } 41 | $this->definition()->setSchema($schema); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Helpers/ConfigResolver.php: -------------------------------------------------------------------------------- 1 | */ 12 | protected array $tableDefinitions = []; 13 | 14 | /** @var array */ 15 | protected array $sorted = []; 16 | 17 | public function __construct(array $tableDefinitions) 18 | { 19 | $this->tableDefinitions = $tableDefinitions; 20 | 21 | $this->build(); 22 | } 23 | 24 | protected function build() 25 | { 26 | /** @var TableDefinition[] $keyedDefinitions */ 27 | $keyedDefinitions = collect($this->tableDefinitions) 28 | ->keyBy(function (TableDefinition $tableDefinition) { 29 | return $tableDefinition->getTableName(); 30 | }); 31 | $dependencies = []; 32 | foreach ($this->tableDefinitions as $tableDefinition) { 33 | $dependencies[$tableDefinition->getTableName()] = []; 34 | } 35 | foreach ($this->tableDefinitions as $tableDefinition) { 36 | foreach ($tableDefinition->getForeignKeyDefinitions() as $indexDefinition) { 37 | if ($indexDefinition->getForeignReferencedTable() === $tableDefinition->getTableName()) { 38 | continue; 39 | } 40 | if (! in_array($indexDefinition->getForeignReferencedTable(), $dependencies[$tableDefinition->getTableName()])) { 41 | $dependencies[$tableDefinition->getTableName()][] = $indexDefinition->getForeignReferencedTable(); 42 | } 43 | } 44 | } 45 | 46 | $sorter = new FixedArraySort(); 47 | $circulars = []; 48 | $sorter->setCircularInterceptor(function ($nodes) use (&$circulars) { 49 | $circulars[] = [$nodes[count($nodes) - 2], $nodes[count($nodes) - 1]]; 50 | }); 51 | foreach ($dependencies as $table => $dependencyArray) { 52 | $sorter->add($table, $dependencyArray); 53 | } 54 | $sorted = $sorter->sort(); 55 | $definitions = collect($sorted)->map(function ($item) use ($keyedDefinitions) { 56 | return $keyedDefinitions[$item]; 57 | })->toArray(); 58 | 59 | foreach ($circulars as $groups) { 60 | [$start, $end] = $groups; 61 | $startDefinition = $keyedDefinitions[$start]; 62 | $indicesForStart = collect($startDefinition->getForeignKeyDefinitions()) 63 | ->filter(function (IndexDefinition $index) use ($end) { 64 | return $index->getForeignReferencedTable() == $end; 65 | }); 66 | foreach ($indicesForStart as $index) { 67 | $startDefinition->removeIndexDefinition($index); 68 | } 69 | if (! in_array($start, $sorted)) { 70 | $definitions[] = $startDefinition; 71 | } 72 | 73 | $endDefinition = $keyedDefinitions[$end]; 74 | 75 | $indicesForEnd = collect($endDefinition->getForeignKeyDefinitions()) 76 | ->filter(function (IndexDefinition $index) use ($start) { 77 | return $index->getForeignReferencedTable() == $start; 78 | }); 79 | foreach ($indicesForEnd as $index) { 80 | $endDefinition->removeIndexDefinition($index); 81 | } 82 | if (! in_array($end, $sorted)) { 83 | $definitions[] = $endDefinition; 84 | } 85 | 86 | $definitions[] = new TableDefinition([ 87 | 'tableName' => $startDefinition->getTableName(), 88 | 'driver' => $startDefinition->getDriver(), 89 | 'columnDefinitions' => [], 90 | 'indexDefinitions' => $indicesForStart->toArray(), 91 | ]); 92 | 93 | $definitions[] = new TableDefinition([ 94 | 'tableName' => $endDefinition->getTableName(), 95 | 'driver' => $endDefinition->getDriver(), 96 | 'columnDefinitions' => [], 97 | 'indexDefinitions' => $indicesForEnd->toArray(), 98 | ]); 99 | } 100 | $this->sorted = $definitions; 101 | } 102 | 103 | /** 104 | * @return TableDefinition[] 105 | */ 106 | public function getDependencyOrder(): array 107 | { 108 | return $this->sorted; 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Helpers/Formatter.php: -------------------------------------------------------------------------------- 1 | tabCharacter = $tabCharacter; 14 | } 15 | 16 | public function line(string $data, $indentTimes = 0) 17 | { 18 | $this->lines[] = str_repeat($this->tabCharacter, $indentTimes).$data; 19 | 20 | return function ($data) use ($indentTimes) { 21 | return $this->line($data, $indentTimes + 1); 22 | }; 23 | } 24 | 25 | public function render($extraIndent = 0) 26 | { 27 | $lines = $this->lines; 28 | if ($extraIndent > 0) { 29 | $lines = collect($lines)->map(function ($item, $index) use ($extraIndent) { 30 | if ($index === 0) { 31 | return $item; 32 | } 33 | 34 | return str_repeat($this->tabCharacter, $extraIndent).$item; 35 | })->toArray(); 36 | } 37 | 38 | return implode("\n", $lines); 39 | } 40 | 41 | public function replaceOnLine($toReplace, $body) 42 | { 43 | if (preg_match('/^(\s+)?'.preg_quote($toReplace).'/m', $body, $matches) !== false) { 44 | $gap = $matches[1] ?? ''; 45 | $numSpaces = strlen($this->tabCharacter); 46 | if ($numSpaces === 0) { 47 | $startingTabIndent = 0; 48 | } else { 49 | $startingTabIndent = (int) (strlen($gap) / $numSpaces); 50 | } 51 | 52 | return preg_replace('/'.preg_quote($toReplace).'/', $this->render($startingTabIndent), $body); 53 | } 54 | 55 | return $body; 56 | } 57 | 58 | public static function replace($tabCharacter, $toReplace, $replacement, $body) 59 | { 60 | $formatter = new static($tabCharacter); 61 | foreach (explode("\n", $replacement) as $line) { 62 | $formatter->line($line); 63 | } 64 | 65 | return $formatter->replaceOnLine($toReplace, $body); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Helpers/ValueToString.php: -------------------------------------------------------------------------------- 1 | map(fn ($item) => $quote.$item.$quote)->implode(', ').']'; 47 | } elseif (is_int($value) || is_float($value)) { 48 | return $value; 49 | } 50 | 51 | if (static::isCastedValue($value)) { 52 | return static::parseCastedValue($value); 53 | } 54 | 55 | if (Str::startsWith($value, $quote) && Str::endsWith($value, $quote)) { 56 | return $value; 57 | } 58 | 59 | return $quote.$value.$quote; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Helpers/WritableTrait.php: -------------------------------------------------------------------------------- 1 | writable = $writable; 12 | 13 | return $this; 14 | } 15 | 16 | public function isWritable() 17 | { 18 | return $this->writable; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/LaravelMigrationGeneratorProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom( 16 | __DIR__.'/../config/laravel-migration-generator.php', 17 | 'laravel-migration-generator' 18 | ); 19 | 20 | $this->publishes([ 21 | __DIR__.'/../stubs' => resource_path('stubs/vendor/laravel-migration-generator'), 22 | __DIR__.'/../config/laravel-migration-generator.php' => config_path('laravel-migration-generator.php'), 23 | ]); 24 | 25 | if ($this->app->runningInConsole()) { 26 | $this->app->instance('laravel-migration-generator:time', now()); 27 | $this->commands([ 28 | GenerateMigrationsCommand::class, 29 | ]); 30 | } 31 | if (config('laravel-migration-generator.run_after_migrations') && config('app.env') === 'local') { 32 | Event::listen(MigrationsEnded::class, function () { 33 | Artisan::call('generate:migrations'); 34 | }); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Tokenizers/BaseColumnTokenizer.php: -------------------------------------------------------------------------------- 1 | definition = new ColumnDefinition(); 15 | parent::__construct($value); 16 | } 17 | 18 | public function definition(): ColumnDefinition 19 | { 20 | return $this->definition; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tokenizers/BaseIndexTokenizer.php: -------------------------------------------------------------------------------- 1 | definition = new IndexDefinition(); 15 | parent::__construct($value); 16 | } 17 | 18 | public function definition(): IndexDefinition 19 | { 20 | return $this->definition; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Tokenizers/BaseTokenizer.php: -------------------------------------------------------------------------------- 1 | value = $value; 20 | $prune = false; 21 | $pruneSingleQuotes = false; 22 | 23 | if (preg_match("/(DEFAULT|COMMENT) ''/", $value, $matches)) { 24 | $value = str_replace($matches[1].' \'\'', $matches[1].' '.self::EMPTY_STRING_REPLACER, $value); 25 | } 26 | 27 | //first get rid of any single quoted stuff with '' around it 28 | if (preg_match_all('/\'\'(.+?)\'\'/', $value, $matches)) { 29 | foreach ($matches[0] as $key => $singleQuoted) { 30 | $toReplace = $singleQuoted; 31 | $value = str_replace($toReplace, self::SINGLE_QUOTE_REPLACER.$matches[1][$key].self::SINGLE_QUOTE_REPLACER, $value); 32 | $pruneSingleQuotes = true; 33 | } 34 | } 35 | 36 | if (preg_match_all("/'(.*?)'/", $value, $matches)) { 37 | foreach ($matches[0] as $quoteWithSpace) { 38 | //we've got an enum or set that has spaces in the text 39 | //so we'll convert to a different character so it doesn't get pruned 40 | $toReplace = $quoteWithSpace; 41 | $value = str_replace($toReplace, str_replace(' ', self::SPACE_REPLACER, $toReplace), $value); 42 | $prune = true; 43 | } 44 | } 45 | $value = str_replace(self::EMPTY_STRING_REPLACER, '\'\'', $value); 46 | $this->tokens = array_map(function ($item) { 47 | return trim($item, ', '); 48 | }, str_getcsv($value, ' ', "'")); 49 | 50 | if ($prune) { 51 | $this->tokens = array_map(function ($item) { 52 | return str_replace(self::SPACE_REPLACER, ' ', $item); 53 | }, $this->tokens); 54 | } 55 | if ($pruneSingleQuotes) { 56 | $this->tokens = array_map(function ($item) { 57 | return str_replace(self::SINGLE_QUOTE_REPLACER, '\'', $item); 58 | }, $this->tokens); 59 | } 60 | } 61 | 62 | public static function make(string $line) 63 | { 64 | return new static($line); 65 | } 66 | 67 | /** 68 | * @return static 69 | */ 70 | public static function parse(string $line) 71 | { 72 | return (new static($line))->tokenize(); 73 | } 74 | 75 | protected function parseColumn($value) 76 | { 77 | return trim($value, '` '); 78 | } 79 | 80 | protected function columnsToArray($string) 81 | { 82 | $string = trim($string, '()'); 83 | 84 | return array_map(fn ($item) => $this->parseColumn($item), explode(',', $string)); 85 | } 86 | 87 | protected function consume() 88 | { 89 | return array_shift($this->tokens); 90 | } 91 | 92 | protected function putBack($value) 93 | { 94 | array_unshift($this->tokens, $value); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Tokenizers/Interfaces/ColumnTokenizerInterface.php: -------------------------------------------------------------------------------- 1 | consumeColumnName(); 24 | $this->consumeColumnType(); 25 | if ($this->isNumberType()) { 26 | $this->consumeUnsigned(); 27 | $this->consumeZeroFill(); 28 | } 29 | if ($this->isTextType()) { 30 | //possibly has a character set 31 | $this->consumeCharacterSet(); 32 | 33 | //has collation data most likely 34 | $this->consumeCollation(); 35 | } 36 | 37 | $this->consumeNullable(); 38 | 39 | $this->consumeDefaultValue(); 40 | if ($this->isNumberType()) { 41 | $this->consumeAutoIncrement(); 42 | $this->consumeKeyConstraints(); 43 | } 44 | 45 | $this->consumeGenerated(); 46 | 47 | if ($this->columnDataType == 'timestamp' || $this->columnDataType == 'datetime') { 48 | $this->consumeTimestamp(); 49 | } 50 | 51 | $this->consumeComment(); 52 | 53 | return $this; 54 | } 55 | 56 | //region Consumers 57 | 58 | protected function consumeColumnName() 59 | { 60 | $this->definition->setColumnName($this->parseColumn($this->consume())); 61 | } 62 | 63 | protected function consumeZeroFill() 64 | { 65 | $nextPiece = $this->consume(); 66 | 67 | if (strtoupper($nextPiece) === 'ZEROFILL') { 68 | $this->zeroFill = true; 69 | } else { 70 | $this->putBack($nextPiece); 71 | } 72 | } 73 | 74 | protected function consumeColumnType() 75 | { 76 | $originalColumnType = $columnType = $this->consume(); 77 | $hasConstraints = Str::contains($columnType, '('); 78 | 79 | if ($hasConstraints) { 80 | $columnType = explode('(', $columnType)[0]; 81 | } 82 | 83 | $this->columnDataType = strtolower($columnType); 84 | 85 | $this->resolveColumnMethod(); 86 | if ($hasConstraints) { 87 | preg_match("/\((.+?)\)/", $originalColumnType, $constraintMatches); 88 | $matches = explode(',', $constraintMatches[1]); 89 | $this->resolveColumnConstraints($matches); 90 | } 91 | } 92 | 93 | private function consumeAutoIncrement() 94 | { 95 | $piece = $this->consume(); 96 | if (strtoupper($piece) === 'AUTO_INCREMENT') { 97 | $this->definition->setAutoIncrementing(true); 98 | } else { 99 | $this->putBack($piece); 100 | } 101 | } 102 | 103 | protected function consumeNullable() 104 | { 105 | $piece = $this->consume(); 106 | if (strtoupper($piece) === 'NOT') { 107 | $this->consume(); //next is NULL 108 | $this->definition->setNullable(false); 109 | } elseif (strtoupper($piece) === 'NULL') { 110 | $this->definition->setNullable(true); 111 | } else { 112 | //something else 113 | $this->putBack($piece); 114 | } 115 | 116 | if (Str::contains($this->columnDataType, 'text')) { 117 | //text column types are explicitly nullable unless set to NOT NULL 118 | if ($this->definition->isNullable() === null) { 119 | $this->definition->setNullable(true); 120 | } 121 | } 122 | } 123 | 124 | protected function consumeDefaultValue() 125 | { 126 | $piece = $this->consume(); 127 | if (strtoupper($piece) === 'DEFAULT') { 128 | $this->definition->setDefaultValue($this->consume()); 129 | 130 | if (strtoupper($this->definition->getDefaultValue()) === 'NULL') { 131 | $this->definition 132 | ->setNullable(true) 133 | ->setDefaultValue(null); 134 | } elseif (strtoupper($this->definition->getDefaultValue()) === 'CURRENT_TIMESTAMP') { 135 | $this->definition 136 | ->setDefaultValue(null) 137 | ->setUseCurrent(true); 138 | } elseif (preg_match("/b'([01]+)'/i", $this->definition->getDefaultValue(), $matches)) { 139 | // Binary digit, so let's convert to PHP's version 140 | $this->definition->setDefaultValue(ValueToString::castBinary($matches[1])); 141 | } 142 | if ($this->definition->getDefaultValue() !== null) { 143 | if ($this->isNumberType()) { 144 | if (Str::contains(strtoupper($this->columnDataType), 'INT')) { 145 | $this->definition->setDefaultValue((int) $this->definition->getDefaultValue()); 146 | } else { 147 | //floats get converted to strings improperly, gotta do a string cast 148 | $this->definition->setDefaultValue(ValueToString::castFloat($this->definition->getDefaultValue())); 149 | } 150 | } else { 151 | if (! $this->isBinaryType()) { 152 | $this->definition->setDefaultValue((string) $this->definition->getDefaultValue()); 153 | } 154 | } 155 | } 156 | } else { 157 | $this->putBack($piece); 158 | } 159 | } 160 | 161 | protected function consumeComment() 162 | { 163 | $piece = $this->consume(); 164 | if (strtoupper($piece) === 'COMMENT') { 165 | // next piece is the comment content 166 | $this->definition->setComment($this->consume()); 167 | } else { 168 | $this->putBack($piece); 169 | } 170 | } 171 | 172 | protected function consumeCharacterSet() 173 | { 174 | $piece = $this->consume(); 175 | 176 | if (strtoupper($piece) === 'CHARACTER') { 177 | $this->consume(); // SET, throw it away 178 | 179 | $this->definition->setCharacterSet($this->consume()); 180 | } else { 181 | //something else 182 | $this->putBack($piece); 183 | } 184 | } 185 | 186 | protected function consumeCollation() 187 | { 188 | $piece = $this->consume(); 189 | if (strtoupper($piece) === 'COLLATE') { 190 | //next piece is the collation type 191 | $this->definition->setCollation($this->consume()); 192 | } else { 193 | $this->putBack($piece); 194 | } 195 | } 196 | 197 | private function consumeUnsigned() 198 | { 199 | $piece = $this->consume(); 200 | if (strtoupper($piece) === 'UNSIGNED') { 201 | $this->definition->setUnsigned(true); 202 | } else { 203 | $this->putBack($piece); 204 | } 205 | } 206 | 207 | private function consumeKeyConstraints() 208 | { 209 | $nextPiece = $this->consume(); 210 | if (strtoupper($nextPiece) === 'PRIMARY') { 211 | $this->definition->setPrimary(true); 212 | 213 | $next = $this->consume(); 214 | if (strtoupper($next) !== 'KEY') { 215 | $this->putBack($next); 216 | } 217 | } elseif (strtoupper($nextPiece) === 'UNIQUE') { 218 | $this->definition->setUnique(true); 219 | 220 | $next = $this->consume(); 221 | if (strtoupper($next) !== 'KEY') { 222 | $this->putBack($next); 223 | } 224 | } else { 225 | $this->putBack($nextPiece); 226 | } 227 | } 228 | 229 | private function consumeGenerated() 230 | { 231 | $canContinue = false; 232 | $nextPiece = $this->consume(); 233 | if (strtoupper($nextPiece) === 'GENERATED') { 234 | $piece = $this->consume(); 235 | if (strtoupper($piece) === 'ALWAYS') { 236 | $this->consume(); // AS 237 | $canContinue = true; 238 | } else { 239 | $this->putBack($piece); 240 | } 241 | } elseif (strtoupper($nextPiece) === 'AS') { 242 | $canContinue = true; 243 | } 244 | 245 | if (! $canContinue) { 246 | $this->putBack($nextPiece); 247 | 248 | return; 249 | } 250 | 251 | $expressionPieces = []; 252 | $parenthesisCounter = 0; 253 | while ($pieceOfExpression = $this->consume()) { 254 | $numOpeningParenthesis = substr_count($pieceOfExpression, '('); 255 | $numClosingParenthesis = substr_count($pieceOfExpression, ')'); 256 | $parenthesisCounter += $numOpeningParenthesis - $numClosingParenthesis; 257 | 258 | $expressionPieces[] = $pieceOfExpression; 259 | 260 | if ($parenthesisCounter === 0) { 261 | break; 262 | } 263 | } 264 | $expression = implode(' ', $expressionPieces); 265 | if (Str::startsWith($expression, '((') && Str::endsWith($expression, '))')) { 266 | $expression = substr($expression, 1, strlen($expression) - 2); 267 | } 268 | 269 | $finalPiece = $this->consume(); 270 | if ($finalPiece !== null && strtoupper($finalPiece) === 'STORED') { 271 | $this->definition->setStoredAs($expression)->setNullable(false); 272 | } else { 273 | $this->definition->setVirtualAs($expression)->setNullable(false); 274 | } 275 | } 276 | 277 | private function consumeTimestamp() 278 | { 279 | $nextPiece = $this->consume(); 280 | if (strtoupper($nextPiece) === 'ON') { 281 | $next = $this->consume(); 282 | if (strtoupper($next) === 'UPDATE') { 283 | $next = $this->consume(); 284 | if (strtoupper($next) === 'CURRENT_TIMESTAMP') { 285 | $this->definition->setUseCurrentOnUpdate(true); 286 | } else { 287 | $this->putBack($next); 288 | } 289 | } else { 290 | $this->putBack($next); 291 | } 292 | } else { 293 | $this->putBack($nextPiece); 294 | } 295 | } 296 | 297 | //endregion 298 | 299 | //region Resolvers 300 | private function resolveColumnMethod() 301 | { 302 | $mapped = [ 303 | 'int' => 'integer', 304 | 'tinyint' => 'tinyInteger', 305 | 'smallint' => 'smallInteger', 306 | 'mediumint' => 'mediumInteger', 307 | 'bigint' => 'bigInteger', 308 | 'varchar' => 'string', 309 | 'tinytext' => 'string', //tinytext is not a valid Blueprint method currently 310 | 'mediumtext' => 'mediumText', 311 | 'longtext' => 'longText', 312 | 'blob' => 'binary', 313 | 'datetime' => 'dateTime', 314 | 'geometrycollection' => 'geometryCollection', 315 | 'linestring' => 'lineString', 316 | 'multilinestring' => 'multiLineString', 317 | 'multipolygon' => 'multiPolygon', 318 | 'multipoint' => 'multiPoint', 319 | ]; 320 | if (isset($mapped[$this->columnDataType])) { 321 | $this->definition->setMethodName($mapped[$this->columnDataType]); 322 | } else { 323 | //do some custom resolution 324 | $this->definition->setMethodName($this->columnDataType); 325 | } 326 | } 327 | 328 | private function resolveColumnConstraints(array $constraints) 329 | { 330 | if ($this->columnDataType === 'char' && count($constraints) === 1 && $constraints[0] == 36) { 331 | //uuid for mysql 332 | $this->definition->setIsUUID(true); 333 | 334 | return; 335 | } 336 | if ($this->isArrayType()) { 337 | $this->definition->setMethodParameters([array_map(fn ($item) => trim($item, '\''), $constraints)]); 338 | } else { 339 | if (Str::contains(strtoupper($this->columnDataType), 'INT')) { 340 | $this->definition->setMethodParameters([]); //laravel does not like display field widths 341 | } else { 342 | if ($this->definition->getMethodName() === 'string') { 343 | if (count($constraints) === 1) { 344 | //has a width set 345 | if ($constraints[0] == Builder::$defaultStringLength) { 346 | $this->definition->setMethodParameters([]); 347 | 348 | return; 349 | } 350 | } 351 | } 352 | $this->definition->setMethodParameters(array_map(fn ($item) => (int) $item, $constraints)); 353 | } 354 | } 355 | } 356 | 357 | //endregion 358 | 359 | protected function isTextType() 360 | { 361 | return Str::contains($this->columnDataType, ['char', 'text', 'set', 'enum']); 362 | } 363 | 364 | protected function isNumberType() 365 | { 366 | return Str::contains($this->columnDataType, ['int', 'decimal', 'float', 'double']); 367 | } 368 | 369 | protected function isArrayType() 370 | { 371 | return Str::contains($this->columnDataType, ['enum', 'set']); 372 | } 373 | 374 | protected function isBinaryType() 375 | { 376 | return Str::contains($this->columnDataType, ['bit']); 377 | } 378 | 379 | /** 380 | * @return mixed 381 | */ 382 | public function getColumnDataType() 383 | { 384 | return $this->columnDataType; 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /src/Tokenizers/MySQL/IndexTokenizer.php: -------------------------------------------------------------------------------- 1 | consumeIndexType(); 12 | if ($this->definition->getIndexType() !== 'primary') { 13 | $this->consumeIndexName(); 14 | } 15 | 16 | if ($this->definition->getIndexType() === 'foreign') { 17 | $this->consumeForeignKey(); 18 | } else { 19 | $this->consumeIndexColumns(); 20 | } 21 | 22 | return $this; 23 | } 24 | 25 | private function consumeIndexType() 26 | { 27 | $piece = $this->consume(); 28 | $upper = strtoupper($piece); 29 | if (in_array($upper, ['PRIMARY', 'UNIQUE', 'FULLTEXT'])) { 30 | $this->definition->setIndexType(strtolower($piece)); 31 | $this->consume(); //just the word KEY 32 | } elseif ($upper === 'KEY') { 33 | $this->definition->setIndexType('index'); 34 | } elseif ($upper === 'CONSTRAINT') { 35 | $this->definition->setIndexType('foreign'); 36 | } 37 | } 38 | 39 | private function consumeIndexName() 40 | { 41 | $piece = $this->consume(); 42 | $this->definition->setIndexName($this->parseColumn($piece)); 43 | } 44 | 45 | private function consumeIndexColumns() 46 | { 47 | $piece = $this->consume(); 48 | $columns = $this->columnsToArray($piece); 49 | 50 | $this->definition->setIndexColumns($columns); 51 | } 52 | 53 | private function consumeForeignKey() 54 | { 55 | $piece = $this->consume(); 56 | if (strtoupper($piece) === 'FOREIGN') { 57 | $this->consume(); //KEY 58 | 59 | $columns = []; 60 | $token = $this->consume(); 61 | 62 | while (! is_null($token)) { 63 | $columns = array_merge($columns, $this->columnsToArray($token)); 64 | $token = $this->consume(); 65 | if (strtoupper($token) === 'REFERENCES') { 66 | $this->putBack($token); 67 | 68 | break; 69 | } 70 | } 71 | $this->definition->setIndexColumns($columns); 72 | 73 | $this->consume(); //REFERENCES 74 | 75 | $referencedTable = $this->parseColumn($this->consume()); 76 | $this->definition->setForeignReferencedTable($referencedTable); 77 | 78 | $referencedColumns = []; 79 | $token = $this->consume(); 80 | while (! is_null($token)) { 81 | $referencedColumns = array_merge($referencedColumns, $this->columnsToArray($token)); 82 | $token = $this->consume(); 83 | if (strtoupper($token) === 'ON') { 84 | $this->putBack($token); 85 | 86 | break; 87 | } 88 | } 89 | 90 | $this->definition->setForeignReferencedColumns($referencedColumns); 91 | 92 | $this->consumeConstraintActions(); 93 | } else { 94 | $this->putBack($piece); 95 | } 96 | } 97 | 98 | private function consumeConstraintActions() 99 | { 100 | while ($token = $this->consume()) { 101 | if (strtoupper($token) === 'ON') { 102 | $actionType = strtolower($this->consume()); //UPDATE 103 | $actionMethod = strtolower($this->consume()); //CASCADE | NO ACTION | SET NULL | SET DEFAULT 104 | if ($actionMethod === 'no') { 105 | $this->consume(); //consume ACTION 106 | $actionMethod = 'restrict'; 107 | } elseif ($actionMethod === 'set') { 108 | $actionMethod = 'set '.$this->consume(); //consume NULL or DEFAULT 109 | } 110 | $currentActions = $this->definition->getConstraintActions(); 111 | $currentActions[$actionType] = $actionMethod; 112 | $this->definition->setConstraintActions($currentActions); 113 | } else { 114 | $this->putBack($token); 115 | 116 | break; 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /stubs/table-create.stub: -------------------------------------------------------------------------------- 1 | Schema::create('[TableName]', function (Blueprint $table) { 2 | [Schema] 3 | }); -------------------------------------------------------------------------------- /stubs/table-modify.stub: -------------------------------------------------------------------------------- 1 | Schema::table('[TableName]', function (Blueprint $table) { 2 | [Schema] 3 | }); -------------------------------------------------------------------------------- /stubs/table.stub: -------------------------------------------------------------------------------- 1 | dropView()); 17 | DB::statement($this->createView()); 18 | } 19 | 20 | /** 21 | * Reverse the migrations. 22 | * 23 | * @return void 24 | */ 25 | public function down() 26 | { 27 | DB::statement($this->dropView()); 28 | } 29 | 30 | private function createView() 31 | { 32 | return <<set('database.default', 'testbench'); 13 | $app['config']->set('database.connections.testbench', [ 14 | 'driver' => 'sqlite', 15 | 'database' => ':memory:', 16 | 'prefix' => '', 17 | ]); 18 | } 19 | 20 | protected function getPackageProviders($app) 21 | { 22 | return [ 23 | LaravelMigrationGeneratorProvider::class, 24 | ]; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/Unit/ColumnDefinitionTest.php: -------------------------------------------------------------------------------- 1 | setIndex(true)->setColumnName('testing')->setMethodName('string'); 14 | $indexDefinition = (new IndexDefinition())->setIndexName('test')->setIndexType('index'); 15 | $columnDefinition->addIndexDefinition($indexDefinition); 16 | 17 | $this->assertEquals('$table->string(\'testing\')->index(\'test\')', $columnDefinition->render()); 18 | } 19 | 20 | public function test_it_prunes_empty_primary_key_index() 21 | { 22 | $columnDefinition = (new ColumnDefinition()) 23 | ->setPrimary(true) 24 | ->setColumnName('testing') 25 | ->setUnsigned(true) 26 | ->setMethodName('integer'); 27 | $indexDefinition = (new IndexDefinition()) 28 | ->setIndexType('primary'); 29 | $columnDefinition->addIndexDefinition($indexDefinition); 30 | 31 | $this->assertEquals('$table->unsignedInteger(\'testing\')->primary()', $columnDefinition->render()); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Unit/DependencyResolverTest.php: -------------------------------------------------------------------------------- 1 | 'tests', 17 | 'columnDefinitions' => [ 18 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 19 | (new ColumnDefinition())->setColumnName('name')->setMethodName('string')->setNullable(false), 20 | ], 21 | 'indexDefinitions' => [], 22 | ]); 23 | 24 | $foreignTableDefinition = new TableDefinition([ 25 | 'tableName' => 'test_items', 26 | 'columnDefinitions' => [ 27 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 28 | (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), 29 | ], 30 | 'indexDefinitions' => [ 31 | (new IndexDefinition())->setIndexName('fk_test_id')->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), 32 | ], 33 | ]); 34 | 35 | $resolver = new DependencyResolver([$tableDefinition, $foreignTableDefinition]); 36 | $order = $resolver->getDependencyOrder(); 37 | $this->assertCount(2, $order); 38 | $this->assertEquals('tests', $order[0]->getTableName()); 39 | $this->assertEquals('test_items', $order[1]->getTableName()); 40 | } 41 | 42 | public function test_it_finds_cyclical_dependencies() 43 | { 44 | $tableDefinition = new TableDefinition([ 45 | 'tableName' => 'tests', 46 | 'driver' => 'mysql', 47 | 'columnDefinitions' => [ 48 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 49 | (new ColumnDefinition())->setColumnName('test_item_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), 50 | ], 51 | 'indexDefinitions' => [ 52 | (new IndexDefinition())->setIndexName('fk_test_item_id')->setIndexColumns(['test_item_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('test_items'), 53 | ], 54 | ]); 55 | 56 | $foreignTableDefinition = new TableDefinition([ 57 | 'tableName' => 'test_items', 58 | 'driver' => 'mysql', 59 | 'columnDefinitions' => [ 60 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 61 | (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), 62 | ], 63 | 'indexDefinitions' => [ 64 | (new IndexDefinition())->setIndexName('fk_test_id')->setIndexColumns(['test_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), 65 | ], 66 | ]); 67 | 68 | $resolver = new DependencyResolver([$tableDefinition, $foreignTableDefinition]); 69 | 70 | $order = $resolver->getDependencyOrder(); 71 | $this->assertCount(4, $order); 72 | $this->assertEquals('$table->foreign(\'test_id\', \'fk_test_id\')->references(\'id\')->on(\'tests\');', $order[2]->formatter()->getSchema()); 73 | $this->assertEquals('$table->foreign(\'test_item_id\', \'fk_test_item_id\')->references(\'id\')->on(\'test_items\');', $order[3]->formatter()->getSchema()); 74 | } 75 | 76 | public function test_it_finds_self_referential_dependencies() 77 | { 78 | $tableDefinition = new TableDefinition([ 79 | 'tableName' => 'tests', 80 | 'driver' => 'mysql', 81 | 'columnDefinitions' => [ 82 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 83 | (new ColumnDefinition())->setColumnName('parent_id')->setMethodName('integer')->setUnsigned(true)->setNullable(false), 84 | ], 85 | 'indexDefinitions' => [ 86 | (new IndexDefinition())->setIndexName('fk_parent_id')->setIndexColumns(['parent_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), 87 | ], 88 | ]); 89 | 90 | $resolver = new DependencyResolver([$tableDefinition]); 91 | 92 | $order = $resolver->getDependencyOrder(); 93 | $this->assertCount(1, $order); 94 | $this->assertCount(2, $order[0]->getColumnDefinitions()); 95 | $this->assertCount(1, $order[0]->getIndexDefinitions()); 96 | 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Unit/FormatterTest.php: -------------------------------------------------------------------------------- 1 | line('Test'); 14 | $this->assertEquals('Test', $formatter->render()); 15 | } 16 | 17 | public function test_can_chain() 18 | { 19 | $formatter = new Formatter(); 20 | $line = $formatter->line('$this->call(function(){'); 21 | $line('$this->die();'); 22 | $formatter->line('});'); 23 | $this->assertEquals(<<<'STR' 24 | $this->call(function(){ 25 | $this->die(); 26 | }); 27 | STR, $formatter->render()); 28 | } 29 | 30 | public function test_can_get_current_line_indent_level() 31 | { 32 | $formatter = new Formatter(); 33 | $formatter->line('Line'); 34 | $formatter->line('Line 2'); 35 | 36 | $body = <<<'STR' 37 | [Test] 38 | STR; 39 | 40 | $replaced = $formatter->replaceOnLine('[Test]', $body); 41 | $shouldEqual = <<<'STR' 42 | Line 43 | Line 2 44 | STR; 45 | $this->assertEquals($shouldEqual, $replaced); 46 | } 47 | 48 | public function test_can_replace_on_no_indent() 49 | { 50 | $replaced = Formatter::replace(' ', '[TEST]', 'Test', '[TEST]'); 51 | $this->assertEquals('Test', $replaced); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/Unit/GeneratorManagers/MySQLGeneratorManagerTest.php: -------------------------------------------------------------------------------- 1 | partialMock(MySQLGeneratorManager::class, function (MockInterface $mock) use ($tableDefinitions) { 18 | $mock->shouldReceive('init', 'createMissingDirectory', 'writeTableMigrations', 'writeViewMigrations'); 19 | $mock->shouldReceive('createMissingDirectory'); 20 | 21 | $mock->shouldReceive('getTableDefinitions')->andReturn($tableDefinitions); 22 | }); 23 | } 24 | 25 | public function test_can_sort_tables() 26 | { 27 | /** @var MySQLGeneratorManager $mocked */ 28 | $mocked = $this->getManagerMock([ 29 | new TableDefinition([ 30 | 'tableName' => 'tests', 31 | 'driver' => 'mysql', 32 | 'columnDefinitions' => [ 33 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 34 | (new ColumnDefinition())->setColumnName('test_item_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), 35 | ], 36 | 'indexDefinitions' => [ 37 | (new IndexDefinition())->setIndexName('fk_test_item_id')->setIndexColumns(['test_item_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('test_items'), 38 | ], 39 | ]), 40 | new TableDefinition([ 41 | 'tableName' => 'test_items', 42 | 'driver' => 'mysql', 43 | 'columnDefinitions' => [ 44 | (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), 45 | (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), 46 | ], 47 | 'indexDefinitions' => [ 48 | (new IndexDefinition())->setIndexName('fk_test_id')->setIndexColumns(['test_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), 49 | ], 50 | ]), 51 | ]); 52 | $sorted = $mocked->sortTables($mocked->getTableDefinitions()); 53 | $this->assertCount(4, $sorted); 54 | $this->assertStringContainsString('$table->dropForeign', $sorted[3]->formatter()->stubTableDown()); 55 | } 56 | 57 | public function test_can_remove_database_prefix() 58 | { 59 | $connection = DB::getDefaultConnection(); 60 | config()->set('database.connections.'.$connection.'.prefix', 'wp_'); 61 | 62 | $mocked = $this->partialMock(MySQLGeneratorManager::class, function (MockInterface $mock) { 63 | $mock->shouldReceive('init'); 64 | }); 65 | 66 | $definition = (new TableDefinition())->setTableName('wp_posts'); 67 | $mocked->addTableDefinition($definition); 68 | $this->assertEquals('posts', $definition->getTableName()); 69 | 70 | $definition = (new TableDefinition())->setTableName('posts'); 71 | $mocked->addTableDefinition($definition); 72 | $this->assertEquals('posts', $definition->getTableName()); 73 | 74 | config()->set('database.connections.'.$connection.'.prefix', ''); 75 | 76 | $definition = (new TableDefinition())->setTableName('wp_posts'); 77 | $mocked->addTableDefinition($definition); 78 | $this->assertEquals('wp_posts', $definition->getTableName()); 79 | 80 | $definition = (new TableDefinition())->setTableName('posts'); 81 | $mocked->addTableDefinition($definition); 82 | $this->assertEquals('posts', $definition->getTableName()); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /tests/Unit/Generators/MySQLTableGeneratorTest.php: -------------------------------------------------------------------------------- 1 | cleanUpMigrations($path); 17 | } 18 | 19 | private function assertSchemaHas($str, $schema) 20 | { 21 | $this->assertStringContainsString($str, $schema); 22 | } 23 | 24 | public function test_runs_correctly() 25 | { 26 | $generator = TableGenerator::init('table', [ 27 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 28 | '`user_id` int(9) unsigned NOT NULL', 29 | '`note` varchar(255) NOT NULL', 30 | 'KEY `fk_user_id_idx` (`user_id`)', 31 | 'CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', 32 | ]); 33 | 34 | $schema = $generator->definition()->formatter()->getSchema(); 35 | $this->assertSchemaHas('$table->increments(\'id\');', $schema); 36 | $this->assertSchemaHas('$table->unsignedInteger(\'user_id\');', $schema); 37 | $this->assertSchemaHas('$table->string(\'note\');', $schema); 38 | $this->assertSchemaHas('$table->foreign(\'user_id\', \'fk_user_id\')->references(\'id\')->on(\'users\')->onDelete(\'cascade\')->onUpdate(\'cascade\');', $schema); 39 | } 40 | 41 | public function test_self_referential_foreign_key() 42 | { 43 | $generator = TableGenerator::init('table', [ 44 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 45 | '`parent_id` int(9) unsigned NOT NULL', 46 | 'KEY `fk_parent_id_idx` (`parent_id`)', 47 | 'CONSTRAINT `fk_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `tables` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', 48 | ]); 49 | 50 | $schema = $generator->definition()->formatter()->getSchema(); 51 | $this->assertSchemaHas('$table->increments(\'id\');', $schema); 52 | $this->assertSchemaHas('$table->unsignedInteger(\'parent_id\');', $schema); 53 | $this->assertSchemaHas('$table->foreign(\'parent_id\', \'fk_parent_id\')->references(\'id\')->on(\'tables\')->onDelete(\'cascade\')->onUpdate(\'cascade\');', $schema); 54 | } 55 | 56 | private function cleanUpMigrations($path) 57 | { 58 | if (is_dir($path)) { 59 | foreach (glob($path.'/*.php') as $file) { 60 | unlink($file); 61 | } 62 | rmdir($path); 63 | } 64 | } 65 | 66 | public function test_writes() 67 | { 68 | Config::set('laravel-migration-generator.table_naming_scheme', '0000_00_00_000000_create_[TableName]_table.php'); 69 | $generator = TableGenerator::init('table', [ 70 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 71 | '`user_id` int(9) unsigned NOT NULL', 72 | '`note` varchar(255) NOT NULL', 73 | 'KEY `fk_user_id_idx` (`user_id`)', 74 | 'CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', 75 | ]); 76 | 77 | $path = __DIR__.'/../../migrations'; 78 | 79 | if (! is_dir($path)) { 80 | mkdir($path, 0777, true); 81 | } 82 | 83 | $generator->definition()->formatter()->write($path); 84 | 85 | $this->assertFileExists($path.'/0000_00_00_000000_create_table_table.php'); 86 | } 87 | 88 | public function test_cleans_up_regular_morphs() 89 | { 90 | $generator = TableGenerator::init('table', [ 91 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 92 | '`user_id` int(9) unsigned NOT NULL', 93 | '`user_type` varchar(255) NOT NULL', 94 | '`note` varchar(255) NOT NULL', 95 | ]); 96 | 97 | $schema = $generator->definition()->formatter()->getSchema(); 98 | $this->assertSchemaHas('$table->morphs(\'user\');', $schema); 99 | } 100 | 101 | public function test_doesnt_clean_up_morph_looking_columns() 102 | { 103 | $generator = TableGenerator::init('table', [ 104 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 105 | '`user_id` varchar(255) NOT NULL', 106 | '`user_type` varchar(255) NOT NULL', 107 | '`note` varchar(255) NOT NULL', 108 | ]); 109 | 110 | $schema = $generator->definition()->formatter()->getSchema(); 111 | $this->assertStringNotContainsString('$table->morphs(\'user\');', $schema); 112 | } 113 | 114 | public function test_cleans_up_uuid_morphs() 115 | { 116 | $generator = TableGenerator::init('table', [ 117 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 118 | '`user_id` char(36) NOT NULL', 119 | '`user_type` varchar(255) NOT NULL', 120 | '`note` varchar(255) NOT NULL', 121 | ]); 122 | 123 | $schema = $generator->definition()->formatter()->getSchema(); 124 | $this->assertSchemaHas('$table->uuidMorphs(\'user\');', $schema); 125 | } 126 | 127 | public function test_cleans_up_uuid_morphs_nullable() 128 | { 129 | $generator = TableGenerator::init('table', [ 130 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', 131 | '`user_id` char(36) DEFAULT NULL', 132 | '`user_type` varchar(255) DEFAULT NULL', 133 | '`note` varchar(255) NOT NULL', 134 | ]); 135 | 136 | $schema = $generator->definition()->formatter()->getSchema(); 137 | $this->assertSchemaHas('$table->nullableUuidMorphs(\'user\');', $schema); 138 | } 139 | 140 | public function test_doesnt_clean_non_auto_inc_id_to_laravel_method() 141 | { 142 | $generator = TableGenerator::init('table', [ 143 | '`id` int(9) unsigned NOT NULL', 144 | 'PRIMARY KEY `id`', 145 | ]); 146 | 147 | $schema = $generator->definition()->formatter()->getSchema(); 148 | $this->assertSchemaHas('$table->unsignedInteger(\'id\')->primary();', $schema); 149 | } 150 | 151 | public function test_does_clean_auto_inc_int_to_laravel_method() 152 | { 153 | $generator = TableGenerator::init('table', [ 154 | '`id` int(9) unsigned NOT NULL AUTO_INCREMENT', 155 | 'PRIMARY KEY `id`', 156 | ]); 157 | 158 | $schema = $generator->definition()->formatter()->getSchema(); 159 | $this->assertSchemaHas('$table->increments(\'id\');', $schema); 160 | } 161 | 162 | public function test_does_clean_auto_inc_big_int_to_laravel_method() 163 | { 164 | $generator = TableGenerator::init('table', [ 165 | '`id` bigint(12) unsigned NOT NULL AUTO_INCREMENT', 166 | 'PRIMARY KEY `id`', 167 | ]); 168 | 169 | $schema = $generator->definition()->formatter()->getSchema(); 170 | $this->assertSchemaHas('$table->id();', $schema); 171 | } 172 | 173 | public function test_doesnt_clean_timestamps_with_use_current() 174 | { 175 | $generator = TableGenerator::init('table', [ 176 | 'id int auto_increment primary key', 177 | 'created_at timestamp not null default CURRENT_TIMESTAMP', 178 | 'updated_at timestamp null on update CURRENT_TIMESTAMP', 179 | ]); 180 | $schema = $generator->definition()->formatter()->getSchema(); 181 | $this->assertSchemaHas('$table->timestamp(\'created_at\')->useCurrent()', $schema); 182 | $this->assertSchemaHas('$table->timestamp(\'updated_at\')->nullable()->useCurrentOnUpdate()', $schema); 183 | } 184 | 185 | public function test_doesnt_clean_timestamps_with_use_current_on_update() 186 | { 187 | $generator = TableGenerator::init('table', [ 188 | 'id int auto_increment primary key', 189 | 'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 190 | 'updated_at timestamp null on update CURRENT_TIMESTAMP', 191 | ]); 192 | $schema = $generator->definition()->formatter()->getSchema(); 193 | $this->assertSchemaHas('$table->timestamp(\'created_at\')->useCurrent()->useCurrentOnUpdate()', $schema); 194 | $this->assertSchemaHas('$table->timestamp(\'updated_at\')->nullable()->useCurrentOnUpdate()', $schema); 195 | } 196 | 197 | public function test_doesnt_clean_timestamps_with_use_defined_datatype_on_timestamp_configuration() 198 | { 199 | config()->set('laravel-migration-generator.definitions.use_defined_datatype_on_timestamp', true); 200 | $generator = TableGenerator::init('table', [ 201 | 'id int auto_increment primary key', 202 | 'created_at datetime NOT NULL', 203 | 'updated_at datetime NOT NULL', 204 | ]); 205 | $schema = $generator->definition()->formatter()->getSchema(); 206 | $this->assertSchemaHas('$table->dateTime(\'created_at\')', $schema); 207 | $this->assertSchemaHas('$table->dateTime(\'updated_at\')', $schema); 208 | } 209 | 210 | public function test_removes_index_from_column_if_fk() 211 | { 212 | $generator = TableGenerator::init('test', [ 213 | '`import_id` bigint(20) unsigned DEFAULT NULL', 214 | '`import_service_id` bigint(20) unsigned DEFAULT NULL', 215 | 'KEY `fk_import_id` (`import_id`)', 216 | 'KEY `fk_import_service_id` (`import_service_id`)', 217 | 'CONSTRAINT `fk_import_id` FOREIGN KEY (`import_id`) REFERENCES `imports` (`id`)', 218 | 'CONSTRAINT `fk_import_service_id` FOREIGN KEY (`import_service_id`) REFERENCES `import_services` (`id`)', 219 | ]); 220 | 221 | $schema = $generator->definition()->formatter()->getSchema(); 222 | $this->assertSchemaHas('$table->unsignedBigInteger(\'import_id\')->nullable();', $schema); 223 | $this->assertSchemaHas('$table->unsignedBigInteger(\'import_service_id\')->nullable();', $schema); 224 | $this->assertSchemaHas('$table->foreign(\'import_id\', \'fk_import_id\')->references(\'id\')->on(\'imports\');', $schema); 225 | $this->assertSchemaHas('$table->foreign(\'import_service_id\', \'fk_import_service_id\')->references(\'id\')->on(\'import_services\');', $schema); 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/Unit/Generators/MySQLViewGeneratorTest.php: -------------------------------------------------------------------------------- 1 | cleanUpMigrations($path); 16 | } 17 | 18 | private function cleanUpMigrations($path) 19 | { 20 | if (is_dir($path)) { 21 | foreach (glob($path.'/*.php') as $file) { 22 | unlink($file); 23 | } 24 | rmdir($path); 25 | } 26 | } 27 | 28 | public function test_generates() 29 | { 30 | $generator = ViewGenerator::init('viewName', 'CREATE ALGORITHM=UNDEFINED DEFINER=`homestead`@`%` SQL SECURITY DEFINER VIEW `view_client_config` AS select `cfg`.`client_id` AS `client_id`,(case when (`cfg`.`client_type_can_edit` = 1) then 1 when (isnull(`cfg`.`client_type_can_edit`) and (`cfg`.`default_can_edit` = 1)) then 1 else 0 end) AS `can_edit` from `table` `cfg`'); 31 | 32 | $this->assertStringStartsWith('CREATE VIEW `view_client_config` AS', $generator->definition()->getSchema()); 33 | } 34 | 35 | public function test_writes() 36 | { 37 | $generator = ViewGenerator::init('viewName', 'CREATE ALGORITHM=UNDEFINED DEFINER=`homestead`@`%` SQL SECURITY DEFINER VIEW `view_client_config` AS select `cfg`.`client_id` AS `client_id`,(case when (`cfg`.`client_type_can_edit` = 1) then 1 when (isnull(`cfg`.`client_type_can_edit`) and (`cfg`.`default_can_edit` = 1)) then 1 else 0 end) AS `can_edit` from `table` `cfg`'); 38 | $path = __DIR__.'/../../migrations'; 39 | 40 | if (! is_dir($path)) { 41 | mkdir($path, 0777, true); 42 | } 43 | $written = $generator->definition()->formatter()->write($path); 44 | $this->assertFileExists($written); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /tests/Unit/Tokenizers/MySQL/IndexTokenizerTest.php: -------------------------------------------------------------------------------- 1 | definition(); 15 | 16 | $this->assertEquals('index', $indexDefinition->getIndexType()); 17 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 18 | 19 | $this->assertEquals('$table->index([\'email\'], \'password_resets_email_index\')', $indexDefinition->render()); 20 | } 21 | 22 | public function test_it_doesnt_use_index_name() 23 | { 24 | config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); 25 | $indexTokenizer = IndexTokenizer::parse('KEY `password_resets_email_index` (`email`)'); 26 | $indexDefinition = $indexTokenizer->definition(); 27 | 28 | $this->assertEquals('index', $indexDefinition->getIndexType()); 29 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 30 | 31 | $this->assertEquals('$table->index([\'email\'])', $indexDefinition->render()); 32 | config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); 33 | } 34 | 35 | //endregion 36 | 37 | //region Primary Key 38 | public function test_it_tokenizes_simple_primary_key() 39 | { 40 | $indexTokenizer = IndexTokenizer::parse('PRIMARY KEY (`id`)'); 41 | $indexDefinition = $indexTokenizer->definition(); 42 | 43 | $this->assertEquals('primary', $indexDefinition->getIndexType()); 44 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 45 | 46 | $this->assertEquals('$table->primary([\'id\'])', $indexDefinition->render()); 47 | } 48 | 49 | public function test_it_tokenizes_two_column_primary_key() 50 | { 51 | $indexTokenizer = IndexTokenizer::parse('PRIMARY KEY (`email`,`token`)'); 52 | $indexDefinition = $indexTokenizer->definition(); 53 | 54 | $this->assertEquals('primary', $indexDefinition->getIndexType()); 55 | $this->assertTrue($indexDefinition->isMultiColumnIndex()); 56 | $this->assertCount(2, $indexDefinition->getIndexColumns()); 57 | $this->assertEqualsCanonicalizing(['email', 'token'], $indexDefinition->getIndexColumns()); 58 | 59 | $this->assertEquals('$table->primary([\'email\', \'token\'])', $indexDefinition->render()); 60 | } 61 | 62 | //endregion 63 | 64 | //region Unique Key 65 | public function test_it_tokenizes_simple_unique_key() 66 | { 67 | $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_unique` (`email`)'); 68 | $indexDefinition = $indexTokenizer->definition(); 69 | 70 | $this->assertEquals('unique', $indexDefinition->getIndexType()); 71 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 72 | 73 | $this->assertEquals('$table->unique([\'email\'], \'users_email_unique\')', $indexDefinition->render()); 74 | } 75 | 76 | public function test_it_doesnt_use_unique_key_index_name() 77 | { 78 | config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', false); 79 | $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_unique` (`email`)'); 80 | $indexDefinition = $indexTokenizer->definition(); 81 | 82 | $this->assertEquals('unique', $indexDefinition->getIndexType()); 83 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 84 | 85 | $this->assertEquals('$table->unique([\'email\'])', $indexDefinition->render()); 86 | config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', true); 87 | } 88 | 89 | public function test_it_tokenizes_two_column_unique_key() 90 | { 91 | $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_location_id_unique` (`email`,`location_id`)'); 92 | $indexDefinition = $indexTokenizer->definition(); 93 | 94 | $this->assertEquals('unique', $indexDefinition->getIndexType()); 95 | $this->assertTrue($indexDefinition->isMultiColumnIndex()); 96 | $this->assertCount(2, $indexDefinition->getIndexColumns()); 97 | $this->assertEqualsCanonicalizing(['email', 'location_id'], $indexDefinition->getIndexColumns()); 98 | 99 | $this->assertEquals('$table->unique([\'email\', \'location_id\'], \'users_email_location_id_unique\')', $indexDefinition->render()); 100 | } 101 | 102 | public function test_it_tokenizes_two_column_unique_key_and_doesnt_use_index_name() 103 | { 104 | config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', false); 105 | $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_location_id_unique` (`email`,`location_id`)'); 106 | $indexDefinition = $indexTokenizer->definition(); 107 | 108 | $this->assertEquals('unique', $indexDefinition->getIndexType()); 109 | $this->assertTrue($indexDefinition->isMultiColumnIndex()); 110 | $this->assertCount(2, $indexDefinition->getIndexColumns()); 111 | $this->assertEqualsCanonicalizing(['email', 'location_id'], $indexDefinition->getIndexColumns()); 112 | 113 | $this->assertEquals('$table->unique([\'email\', \'location_id\'])', $indexDefinition->render()); 114 | config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', true); 115 | } 116 | 117 | //endregion 118 | 119 | //region Foreign Constraints 120 | public function test_it_tokenizes_foreign_key() 121 | { 122 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)'); 123 | $indexDefinition = $indexTokenizer->definition(); 124 | 125 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 126 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 127 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 128 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 129 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 130 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 131 | 132 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')', $indexDefinition->render()); 133 | } 134 | 135 | public function test_it_tokenizes_foreign_key_doesnt_use_index_name() 136 | { 137 | config()->set('laravel-migration-generator.definitions.use_defined_foreign_key_index_names', false); 138 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)'); 139 | $indexDefinition = $indexTokenizer->definition(); 140 | 141 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 142 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 143 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 144 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 145 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 146 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 147 | 148 | $this->assertEquals('$table->foreign(\'user_id\')->references(\'id\')->on(\'users\')', $indexDefinition->render()); 149 | config()->set('laravel-migration-generator.definitions.use_defined_foreign_key_index_names', true); 150 | } 151 | 152 | public function test_it_tokenizes_foreign_key_with_update() 153 | { 154 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE'); 155 | $indexDefinition = $indexTokenizer->definition(); 156 | 157 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 158 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 159 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 160 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 161 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 162 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 163 | 164 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'cascade\')', $indexDefinition->render()); 165 | } 166 | 167 | public function test_it_tokenizes_foreign_key_with_delete() 168 | { 169 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE'); 170 | $indexDefinition = $indexTokenizer->definition(); 171 | 172 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 173 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 174 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 175 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 176 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 177 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 178 | 179 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onDelete(\'cascade\')', $indexDefinition->render()); 180 | } 181 | 182 | public function test_it_tokenizes_foreign_key_with_update_and_delete() 183 | { 184 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'); 185 | $indexDefinition = $indexTokenizer->definition(); 186 | 187 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 188 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 189 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 190 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 191 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 192 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 193 | 194 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'cascade\')->onDelete(\'cascade\')', $indexDefinition->render()); 195 | } 196 | 197 | public function test_it_tokenizes_foreign_key_with_multiple_columns() 198 | { 199 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `table2_ibfk_1` FOREIGN KEY (`table2-foreign1`, `table2-foreign2`) REFERENCES `table1` (`table1-field1`, `table1-field2`) ON DELETE CASCADE ON UPDATE CASCADE'); 200 | $definition = $indexTokenizer->definition(); 201 | 202 | $this->assertEquals('foreign', $definition->getIndexType()); 203 | $this->assertTrue($definition->isMultiColumnIndex()); 204 | $this->assertCount(2, $definition->getIndexColumns()); 205 | $this->assertEquals('table1', $definition->getForeignReferencedTable()); 206 | $this->assertSame(['table1-field1', 'table1-field2'], $definition->getForeignReferencedColumns()); 207 | $this->assertSame(['table2-foreign1', 'table2-foreign2'], $definition->getIndexColumns()); 208 | $this->assertEquals('$table->foreign([\'table2-foreign1\', \'table2-foreign2\'], \'table2_ibfk_1\')->references([\'table1-field1\', \'table1-field2\'])->on(\'table1\')->onDelete(\'cascade\')->onUpdate(\'cascade\')', $definition->render()); 209 | } 210 | 211 | public function test_it_tokenizes_foreign_key_with_update_restrict() 212 | { 213 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION'); 214 | $indexDefinition = $indexTokenizer->definition(); 215 | 216 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 217 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 218 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 219 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 220 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 221 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 222 | 223 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'restrict\')', $indexDefinition->render()); 224 | } 225 | 226 | public function test_it_tokenizes_foreign_key_with_update_set_null() 227 | { 228 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE SET NULL'); 229 | $indexDefinition = $indexTokenizer->definition(); 230 | 231 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 232 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 233 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 234 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 235 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 236 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 237 | 238 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'set NULL\')', $indexDefinition->render()); 239 | } 240 | 241 | public function test_it_tokenizes_foreign_key_with_update_set_default() 242 | { 243 | $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE SET DEFAULT'); 244 | $indexDefinition = $indexTokenizer->definition(); 245 | 246 | $this->assertEquals('foreign', $indexDefinition->getIndexType()); 247 | $this->assertFalse($indexDefinition->isMultiColumnIndex()); 248 | $this->assertCount(1, $indexDefinition->getIndexColumns()); 249 | $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); 250 | $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); 251 | $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); 252 | 253 | $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'set DEFAULT\')', $indexDefinition->render()); 254 | } 255 | 256 | //endregion 257 | } 258 | --------------------------------------------------------------------------------