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