Repository: bennett-treptow/laravel-migration-generator Branch: main Commit: 14648d21c1df Files: 66 Total size: 237.2 KB Directory structure: gitextract_qojz53lt/ ├── .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 └── ValueToStringTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms github: [bennett-treptow] custom: ['https://buymeacoffee.com/btreptow'] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: "[BUG]" labels: '' assignees: '' --- **Package Version** What version are you running? 2.2.*, 3.* **Database Version** What database driver are you using? And what version is that database? **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Please include any stack traces and applicable .env / config changes you've made **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Additional context** Add any other context about the problem here. ================================================ FILE: .github/workflows/php.yml ================================================ name: PHP Composer on: push: branches: - main pull_request: branches: - main jobs: build: strategy: matrix: php: ['7.4', '8.0', '8.1', '8.2', '8.3', '8.4'] runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} - name: Cache Composer packages id: composer-cache uses: actions/cache@v4 with: path: vendor key: "${{ runner.os }}-php-${{ matrix.php }}-${{ hashFiles('**/composer.lock') }}" restore-keys: | ${{ runner.os }}-php-${{ matrix.php }}- - name: Remove pint run: composer remove laravel/pint --dev --no-update - name: Install dependencies run: composer install --prefer-dist --no-progress - name: Run test suite run: composer run test ================================================ FILE: .gitignore ================================================ vendor/ .idea .phpunit.result.cache .php_cs.cache composer.lock ================================================ FILE: CHANGELOG.md ================================================ # Version 3.1.6 ### New Modifier `useCurrentOnUpdate` has been implemented ### Bugfix Issue #27 - `useCurrent` on `timestamps()` method fix # Version 3.1.3 ### [Timestamp:format] Removal 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. # Version 3.1.0 ### Environment Variables New environment variables: | Key | Default Value | Allowed Values | Description | | --- | ------------- | -------------- | ----------- | | LMG_SKIP_VIEWS | false | boolean | When true, skip all views | | LMG_SKIPPABLE_VIEWS | '' | comma delimited string | The views to be skipped | | LMG_MYSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `mysql` | | LMG_SQLITE_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlite` | | LMG_PGSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `pgsql` | | LMG_SQLSRV_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlsrv` | # Version 3.0.0 ### Run after migrations 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`. ### Environment Variables New environment variables to replace config updates: | Key | Default Value | Allowed Values | Description | | --- | ------------- | -------------- | ----------- | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | | LMG_CLEAR_OUTPUT_PATH | false | boolean | Whether or not to clear out the output path before creating new files | | LMG_TABLE_NAMING_SCHEME | [Timestamp]_create_[TableName]_table.php | string | The string to be used to name table migration files | | LMG_VIEW_NAMING_SCHEME | [Timestamp]_create_[ViewName]_view.php | string | The string to be used to name view migration files | | LMG_OUTPUT_PATH | tests/database/migrations | string | The path (relative to the root of your project) to where the files will be output to | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | | 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 | | 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 | | 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 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 Bennett Treptow Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Laravel Migration Generator ![Latest Version on Packagist](https://img.shields.io/packagist/v/bennett-treptow/laravel-migration-generator.svg) 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. Another use case would be taking a project with a database and no migrations and turning that database into base migrations. # Installation ```bash composer require --dev bennett-treptow/laravel-migration-generator ``` ```bash php artisan vendor:publish --provider="LaravelMigrationGenerator\LaravelMigrationGeneratorProvider" ``` # Lumen Installation ```bash composer require --dev bennett-treptow/laravel-migration-generator ``` Copy config file from `vendor/bennett-treptow/laravel-migration-generator/config` to your Lumen config folder Register service provider in bootstrap/app.php ```php $app->register(\LaravelMigrationGenerator\LaravelMigrationGeneratorProvider::class); ``` # Usage Whenever you have database changes or are ready to squash your database structure down to migrations, run: ```bash php artisan generate:migrations ``` By default, the migrations will be created in `tests/database/migrations`. You can specify a different path with the `--path` option: ```bash php artisan generate:migrations --path=database/migrations ``` You can specify the connection to use as the database with the `--connection` option: ```bash php artisan generate:migrations --connection=mysql2 ``` You can also clear the directory with the `--empty-path` option: ```bash php artisan generate:migrations --empty-path ``` 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`. # Configuration Want to customize the migration stubs? Make sure you've published the vendor assets with the artisan command to publish vendor files above. ## Environment Variables | Key | Default Value | Allowed Values | Description | | --- | ------------- | -------------- | ----------- | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | | 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 | | LMG_TABLE_NAMING_SCHEME | `[Timestamp]_create_[TableName]_table.php` | string | The string to be used to name table migration files | | LMG_VIEW_NAMING_SCHEME | `[Timestamp]_create_[ViewName]_view.php` | string | The string to be used to name view migration files | | 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 | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | | LMG_SKIP_VIEWS | false | boolean | When true, skip all views | | LMG_SKIPPABLE_VIEWS | '' | comma delimited string | The views to be skipped | | LMG_SORT_MODE | 'foreign_key' | string | The sorting mode to be used. Options: `foreign_key` | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | | 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 | | 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 | | 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 | | LMG_WITH_COMMENTS | true | boolean | When true, export comment using `->comment()` method. | | 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 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | | LMG_MYSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `mysql` | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | | LMG_SQLITE_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlite` | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | | LMG_PGSQL_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `pgsql` | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | | LMG_SQLSRV_SKIPPABLE_VIEWS | null | comma delimited string | The views to be skipped when driver is `sqlsrv` | ## Stubs There is a default stub for tables and views, found in `resources/stubs/vendor/laravel-migration-generator/`. 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. ## Stub Naming 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. ### Table Name Stub Tokens Table stubs have the following tokens available for the naming scheme: | Token | Example | Description | | ----- |-------- | ----------- | | `[TableName]` | users | Table's name, same as what is defined in the database | | `[TableName:Studly]` | Users | Table's name with `Str::studly()` applied to it (useful for standardizing table names if they are inconsistent) | | `[TableName:Lowercase]` | users | Table's name with `strtolower` applied to it (useful for standardizing table names if they are inconsistent) | | `[Timestamp]` | 2021_04_25_110000 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` | | `[Index]` | 0 | The key of the migration in the sorted order, for use with enforcing a sort order | | `[IndexedEmptyTimestamp]` | 0000_00_00_000041 | The standard migration timestamp format, but filled with 0s and incremented by `[Index]` seconds | | `[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 | ### Table Schema Stub Tokens Table schema stubs have the following tokens available: | Token | Description | | ----- | ----------- | | `[TableName]` | Table's name, same as what is defined in the database | | `[TableName:Studly]` | Table's name with `Str::studly()` applied to it, for use with the class name | | `[TableUp]` | Table's `up()` function | | `[TableDown]` | Table's `down()` function | | `[Schema]` | The table's generated schema | ### View Name Stub Tokens View stubs have the following tokens available for the naming scheme: | Token | Example | Description | | ----- |-------- | ----------- | | `[ViewName]` | user_earnings | View's name, same as what is defined in the database | | `[ViewName:Studly]` | UserEarnings | View's name with `Str::studly()` applied to it (useful for standardizing view names if they are inconsistent) | | `[ViewName:Lowercase]` | user_earnings | View's name with `strtolower` applied to it (useful for standardizing view names if they are inconsistent) | | `[Timestamp]` | 2021_04_25_110000 | The standard migration timestamp format, at the time of calling the command: `Y_m_d_His` | | `[Index]` | 0 | The key of the migration in the sorted order, for use with enforcing a sort order | | `[IndexedEmptyTimestamp]` | 0000_00_00_000041 | The standard migration timestamp format, but filled with 0s and incremented by `[Index]` seconds | | `[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 | ### View Schema Stub Tokens View schema stubs have the following tokens available: | Token | Description | | ----- | ----------- | | `[ViewName]` | View's name, same as what is defined in the database | | `[ViewName:Studly]` | View's name with `Str::studly()` applied to it, for use with the class name | | `[Schema]` | The view's schema | # Example Usage Given a database structure for a `users` table of: ```sql CREATE TABLE `users` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `username` varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `password` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL, `first_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `last_name` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `timezone` varchar(45) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'America/New_York', `location_id` int(10) unsigned NOT NULL, `deleted_at` timestamp NULL DEFAULT NULL, `remember_token` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL, `created_at` timestamp NULL DEFAULT NULL, `updated_at` timestamp NULL DEFAULT NULL, PRIMARY KEY (`id`), KEY `users_username_index` (`username`), KEY `users_first_name_index` (`first_name`), KEY `users_last_name_index` (`last_name`), KEY `users_email_index` (`email`), KEY `fk_users_location_id_index` (`location_id`) CONSTRAINT `users_location_id_foreign` FOREIGN KEY (`location_id`) REFERENCES `locations` (`id`) ON UPDATE CASCADE ON DELETE CASCADE ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ``` A `tests/database/migrations/[TIMESTAMP]_create_users_table.php` with the following Blueprint would be created: ```php increments('id'); $table->string('username', 128)->nullable()->index(); $table->string('email', 255)->index(); $table->string('password', 255); $table->string('first_name', 45)->nullable()->index(); $table->string('last_name', 45)->index(); $table->string('timezone', 45)->default('America/New_York'); $table->unsignedInteger('location_id'); $table->softDeletes(); $table->string('remember_token', 255)->nullable(); $table->timestamps(); $table->foreign('location_id', 'users_location_id_foreign')->references('id')->on('locations')->onUpdate('cascade')->onDelete('cascade'); }); } /** * Reverse the migrations. * * @return void */ public function down() { Schema::dropIfExists('users'); } } ``` # Currently Supported DBMS's 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) - [x] MySQL - [ ] Postgres - [ ] SQLite - [ ] SQL Server ================================================ FILE: UPGRADE.md ================================================ ## Upgrade from 3.* to 4.0 ### Foreign Key Sorting New foreign key dependency sorting options, available as an env variable to potentially not sort by foreign key dependencies if not necessary. Update your `config/laravel-migration-generator.php` to have a new `sort_mode` key: ```dotenv 'sort_mode' => env('LMG_SORT_MODE', 'foreign_key'), ``` ### New Stubs New stubs for a `create` and a `modify` version for tables. 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. ### New Table and View Naming Tokens Three new tokens were added for the table stubs: `[Index]`, `[IndexedEmptyTimestamp]`, and `[IndexedTimestamp]`. For use with foreign key / sorting in general to enforce a final sorting. `[Index]` is the numeric key (0,1,2,...) that the migration holds in the sort order. `[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` `[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. ### New Table Stub Tokens Two new tokens were added for table stubs: `[TableUp]` and `[TableDown]`. See latest `stubs/table.stub`. It is suggested to upgrade all of your stubs using the latest stubs available by `vendor:publish --force` ## Upgrade from 2.2.* to 3.0.0 `skippable_tables` is now a comma delimited string instead of an array so they are compatible with .env files. All config options have been moved to equivalent .env variables. Please update `config/laravel-migration-generator.php` with a `vendor:publish --force`. The new environment variables are below: | Key | Default Value | Allowed Values | Description | | --- | ------------- | -------------- | ----------- | | LMG_RUN_AFTER_MIGRATIONS | false | boolean | Whether or not the migration generator should run after migrations have completed. | | LMG_CLEAR_OUTPUT_PATH | false | boolean | Whether or not to clear out the output path before creating new files | | LMG_TABLE_NAMING_SCHEME | [Timestamp]_create_[TableName]_table.php | string | The string to be used to name table migration files | | LMG_VIEW_NAMING_SCHEME | [Timestamp]_create_[ViewName]_view.php | string | The string to be used to name view migration files | | LMG_OUTPUT_PATH | tests/database/migrations | string | The path (relative to the root of your project) to where the files will be output to | | LMG_SKIPPABLE_TABLES | migrations | comma delimited string | The tables to be skipped | | LMG_PREFER_UNSIGNED_PREFIX | true | boolean | When true, uses `unsigned` variant methods instead of the `->unsigned()` modifier. | | LMG_USE_DEFINED_INDEX_NAMES | true | boolean | When true, uses index names defined by the database as the name parameter for index methods | | 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 | | 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 | | 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 | | LMG_MYSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `mysql`. | | LMG_MYSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `mysql`. | | LMG_MYSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `mysql`. | | LMG_SQLITE_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlite`. | | LMG_SQLITE_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlite`. | | LMG_SQLITE_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlite`. | | LMG_PGSQL_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `pgsql`. | | LMG_PGSQL_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `pgsql`. | | LMG_PGSQL_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `pgsql`. | | LMG_SQLSRV_TABLE_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_TABLE_NAMING_SCHEME when the database driver is `sqlsrc`. | | LMG_SQLSRV_VIEW_NAMING_SCHEME | null | ?boolean | When not null, this setting will override LMG_VIEW_NAMING_SCHEME when the database driver is `sqlsrv`. | | LMG_SQLSRV_OUTPUT_PATH | null | ?boolean | When not null, this setting will override LMG_OUTPUT_PATH when the database driver is `sqlsrv`. | | LMG_SQLSRV_SKIPPABLE_TABLES | null | ?boolean | When not null, this setting will override LMG_SKIPPABLE_TABLES when the database driver is `sqlsrv`. | ================================================ FILE: composer.json ================================================ { "name": "bennett-treptow/laravel-migration-generator", "description": "Generate migrations from existing database structures", "minimum-stability": "stable", "license": "MIT", "authors": [ { "name": "Bennett Treptow", "email": "me@btreptow.com" } ], "require": { "php": "^7.4|^8.0|^8.1|^8.2|^8.3|^8.4", "illuminate/support": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/console": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/database": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "illuminate/config": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0|^12.0", "marcj/topsort": "^2.0" }, "autoload": { "psr-4": { "LaravelMigrationGenerator\\": "src/" } }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, "require-dev": { "orchestra/testbench": "^6.17|^8.0|^9.0|^10.0", "laravel/pint": "^1.15" }, "scripts": { "post-autoload-dump": [ "@php ./vendor/bin/testbench package:discover --ansi" ], "test": [ "vendor/bin/phpunit" ], "lint": "vendor/bin/pint" }, "extra": { "laravel": { "providers": [ "LaravelMigrationGenerator\\LaravelMigrationGeneratorProvider" ] } }, "prefer-stable": true, "config": { "audit": { "block-insecure": false } } } ================================================ FILE: config/laravel-migration-generator.php ================================================ env('LMG_RUN_AFTER_MIGRATIONS', false), 'clear_output_path' => env('LMG_CLEAR_OUTPUT_PATH', false), //default configs 'table_naming_scheme' => env('LMG_TABLE_NAMING_SCHEME', '[IndexedTimestamp]_create_[TableName]_table.php'), 'view_naming_scheme' => env('LMG_VIEW_NAMING_SCHEME', '[IndexedTimestamp]_create_[ViewName]_view.php'), 'path' => env('LMG_OUTPUT_PATH', 'tests/database/migrations'), 'skippable_tables' => env('LMG_SKIPPABLE_TABLES', 'migrations'), 'skip_views' => env('LMG_SKIP_VIEWS', false), 'skippable_views' => env('LMG_SKIPPABLE_VIEWS', ''), 'sort_mode' => env('LMG_SORT_MODE', 'foreign_key'), 'definitions' => [ 'prefer_unsigned_prefix' => env('LMG_PREFER_UNSIGNED_PREFIX', true), 'use_defined_index_names' => env('LMG_USE_DEFINED_INDEX_NAMES', true), 'use_defined_foreign_key_index_names' => env('LMG_USE_DEFINED_FOREIGN_KEY_INDEX_NAMES', true), 'use_defined_unique_key_index_names' => env('LMG_USE_DEFINED_UNIQUE_KEY_INDEX_NAMES', true), 'use_defined_primary_key_index_names' => env('LMG_USE_DEFINED_PRIMARY_KEY_INDEX_NAMES', true), 'with_comments' => env('LMG_WITH_COMMENTS', true), 'use_defined_datatype_on_timestamp' => env('LMG_USE_DEFINED_DATATYPE_ON_TIMESTAMP', false), ], //now driver specific configs //null = use default 'mysql' => [ 'table_naming_scheme' => env('LMG_MYSQL_TABLE_NAMING_SCHEME', null), 'view_naming_scheme' => env('LMG_MYSQL_VIEW_NAMING_SCHEME', null), 'path' => env('LMG_MYSQL_OUTPUT_PATH', null), 'skippable_tables' => env('LMG_MYSQL_SKIPPABLE_TABLES', null), 'skippable_views' => env('LMG_MYSQL_SKIPPABLE_VIEWS', null), ], 'sqlite' => [ 'table_naming_scheme' => env('LMG_SQLITE_TABLE_NAMING_SCHEME', null), 'view_naming_scheme' => env('LMG_SQLITE_VIEW_NAMING_SCHEME', null), 'path' => env('LMG_SQLITE_OUTPUT_PATH', null), 'skippable_tables' => env('LMG_SQLITE_SKIPPABLE_TABLES', null), 'skippable_views' => env('LMG_SQLITE_SKIPPABLE_VIEWS', null), ], 'pgsql' => [ 'table_naming_scheme' => env('LMG_PGSQL_TABLE_NAMING_SCHEME', null), 'view_naming_scheme' => env('LMG_PGSQL_VIEW_NAMING_SCHEME', null), 'path' => env('LMG_PGSQL_OUTPUT_PATH', null), 'skippable_tables' => env('LMG_PGSQL_SKIPPABLE_TABLES', null), 'skippable_views' => env('LMG_PGSQL_SKIPPABLE_VIEWS', null), ], 'sqlsrv' => [ 'table_naming_scheme' => env('LMG_SQLSRV_TABLE_NAMING_SCHEME', null), 'view_naming_scheme' => env('LMG_SQLSRV_VIEW_NAMING_SCHEME', null), 'path' => env('LMG_SQLSRV_OUTPUT_PATH', null), 'skippable_tables' => env('LMG_SQLSRV_SKIPPABLE_TABLES', null), 'skippable_views' => env('LMG_SQLSRV_SKIPPABLE_VIEWS', null), ], ]; ================================================ FILE: docs/_config.yml ================================================ remote_theme: pmarsceill/just-the-docs compress_html: ignore: envs: all aux_links: "View on GitHub": - "//github.com/bennett-treptow/laravel-migration-generator" ================================================ FILE: docs/command.md ================================================ --- layout: default title: Running the Generator nav_order: 3 --- ================================================ FILE: docs/config.md ================================================ --- layout: default title: Config nav_order: 1 --- ================================================ FILE: docs/index.md ================================================ --- layout: default title: Laravel Migration Generator nav_order: 0 --- # Laravel Migration Generator 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. ## Quick Start ```bash composer require --dev bennett-treptow/laravel-migration-generator php artisan vendor:publish --provider="LaravelMigrationGenerator\LaravelMigrationGeneratorProvider" ``` Learn more about [config options](config.md) and [stubs](stubs.md). ================================================ FILE: docs/stubs.md ================================================ --- layout: default title: Stubs nav_order: 2 --- ================================================ FILE: phpunit.xml ================================================ ./tests/Unit ================================================ FILE: pint.json ================================================ { "preset": "laravel" } ================================================ FILE: src/Commands/GenerateMigrationsCommand.php ================================================ option('connection'); if ($connection === 'default') { $connection = Config::get('database.default'); } if (! Config::has('database.connections.'.$connection)) { throw new \Exception('Could not find connection `'.$connection.'` in your config.'); } return $connection; } public function getPath($driver) { $basePath = $this->option('path'); if ($basePath === 'default') { $basePath = ConfigResolver::path($driver); } return $basePath; } public function handle() { try { $connection = $this->getConnection(); } catch (\Exception $e) { $this->error($e->getMessage()); return 1; } $this->info('Using connection '.$connection); DB::setDefaultConnection($connection); $driver = Config::get('database.connections.'.$connection)['driver']; $manager = $this->resolveGeneratorManager($driver); if ($manager === false) { $this->error('The `'.$driver.'` driver is not supported at this time.'); return 1; } $basePath = base_path($this->getPath($driver)); if ($this->option('empty-path') || config('laravel-migration-generator.clear_output_path')) { foreach (glob($basePath.'/*.php') as $file) { unlink($file); } } $this->info('Using '.$basePath.' as the output path..'); $tableNames = Arr::wrap($this->option('table')); $viewNames = Arr::wrap($this->option('view')); $manager->handle($basePath, $tableNames, $viewNames); } /** * @return false|GeneratorManagerInterface */ protected function resolveGeneratorManager(string $driver) { $supported = [ 'mysql' => MySQLGeneratorManager::class, ]; if (! isset($supported[$driver])) { return false; } return new $supported[$driver](); } } ================================================ FILE: src/Definitions/ColumnDefinition.php ================================================ $value) { if (property_exists($this, $attribute)) { $this->$attribute = $value; } } } //region Getters public function getMethodName(): string { return $this->methodName; } public function getMethodParameters(): array { return $this->methodParameters; } public function getColumnName(): ?string { return $this->columnName; } public function isUnsigned(): bool { return $this->unsigned; } /** * @return ?bool */ public function isNullable(): ?bool { return $this->nullable; } /** * @return mixed */ public function getDefaultValue() { if (ValueToString::isCastedValue($this->defaultValue)) { return ValueToString::parseCastedValue($this->defaultValue); } return $this->defaultValue; } public function getComment(): ?string { return $this->comment; } public function getCharacterSet(): ?string { return $this->characterSet; } public function getCollation(): ?string { return $this->collation; } public function isAutoIncrementing(): bool { return $this->autoIncrementing; } public function isIndex(): bool { return $this->index; } public function isPrimary(): bool { return $this->primary; } public function isUnique(): bool { return $this->unique; } public function useCurrent(): bool { return $this->useCurrent; } public function useCurrentOnUpdate(): bool { return $this->useCurrentOnUpdate; } public function getStoredAs(): ?string { return $this->storedAs; } public function getVirtualAs(): ?string { return $this->virtualAs; } public function isUUID(): bool { return $this->isUUID; } //endregion //region Setters public function setMethodName(string $methodName): ColumnDefinition { $this->methodName = $methodName; return $this; } public function setMethodParameters(array $methodParameters): ColumnDefinition { $this->methodParameters = $methodParameters; return $this; } public function setColumnName(?string $columnName): ColumnDefinition { $this->columnName = $columnName; return $this; } public function setUnsigned(bool $unsigned): ColumnDefinition { $this->unsigned = $unsigned; return $this; } /** * @param ?bool $nullable */ public function setNullable(?bool $nullable): ColumnDefinition { $this->nullable = $nullable; return $this; } /** * @param mixed $defaultValue * @return ColumnDefinition */ public function setDefaultValue($defaultValue) { $this->defaultValue = $defaultValue; return $this; } public function setComment(?string $comment): ColumnDefinition { $this->comment = $comment; return $this; } /** * @param string|null $collation */ public function setCharacterSet(?string $characterSet): ColumnDefinition { $this->characterSet = $characterSet; return $this; } public function setCollation(?string $collation): ColumnDefinition { $this->collation = $collation; return $this; } public function setAutoIncrementing(bool $autoIncrementing): ColumnDefinition { $this->autoIncrementing = $autoIncrementing; return $this; } public function setStoredAs(?string $storedAs): ColumnDefinition { $this->storedAs = $storedAs; return $this; } public function setVirtualAs(?string $virtualAs): ColumnDefinition { $this->virtualAs = $virtualAs; return $this; } public function addIndexDefinition(IndexDefinition $definition): ColumnDefinition { $this->indexDefinitions[] = $definition; return $this; } public function setIndex(bool $index): ColumnDefinition { $this->index = $index; return $this; } public function setPrimary(bool $primary): ColumnDefinition { $this->primary = $primary; return $this; } public function setUnique(bool $unique): ColumnDefinition { $this->unique = $unique; return $this; } public function setUseCurrent(bool $useCurrent): ColumnDefinition { $this->useCurrent = $useCurrent; return $this; } public function setUseCurrentOnUpdate(bool $useCurrentOnUpdate): ColumnDefinition { $this->useCurrentOnUpdate = $useCurrentOnUpdate; return $this; } public function setIsUUID(bool $isUUID): ColumnDefinition { $this->isUUID = $isUUID; return $this; } //endregion protected function isNullableMethod($methodName) { return ! in_array($methodName, ['softDeletes', 'morphs', 'nullableMorphs', 'rememberToken', 'nullableUuidMorphs']) && ! $this->isPrimaryKeyMethod($methodName); } protected function isPrimaryKeyMethod($methodName) { return in_array($methodName, ['tinyIncrements', 'mediumIncrements', 'increments', 'bigIncrements', 'id']); } protected function canBeUnsigned($methodName) { return ! in_array($methodName, ['morphs', 'nullableMorphs']) && ! $this->isPrimaryKeyMethod($methodName); } protected function guessLaravelMethod() { if ($this->primary && $this->unsigned && $this->autoIncrementing) { //some sort of increments field if ($this->methodName === 'bigInteger') { if ($this->columnName === 'id') { return [null, 'id', []]; } else { return [$this->columnName, 'bigIncrements', []]; } } elseif ($this->methodName === 'mediumInteger') { return [$this->columnName, 'mediumIncrements', []]; } elseif ($this->methodName === 'integer') { return [$this->columnName, 'increments', []]; } elseif ($this->methodName === 'smallInteger') { return [$this->columnName, 'smallIncrements', []]; } elseif ($this->methodName === 'tinyInteger') { return [$this->columnName, 'tinyIncrements', []]; } } if ($this->methodName === 'tinyInteger' && ! $this->unsigned) { $boolean = false; if (in_array($this->defaultValue, ['true', 'false', true, false, 'TRUE', 'FALSE', '1', '0', 1, 0], true)) { $boolean = true; } if (Str::startsWith(strtoupper($this->columnName), ['IS_', 'HAS_'])) { $boolean = true; } if ($boolean) { return [$this->columnName, 'boolean', []]; } } if ($this->methodName === 'morphs' && $this->nullable === true) { return [$this->columnName, 'nullableMorphs', []]; } if ($this->methodName === 'uuidMorphs' && $this->nullable === true) { return [$this->columnName, 'nullableUuidMorphs', []]; } if ($this->methodName === 'string' && $this->columnName === 'remember_token' && $this->nullable === true) { return [null, 'rememberToken', []]; } if ($this->isUUID() && $this->methodName !== 'uuidMorphs') { //only override if not already uuidMorphs return [$this->columnName, 'uuid', []]; } if (config('laravel-migration-generator.definitions.prefer_unsigned_prefix') && $this->unsigned) { $availableUnsignedPrefixes = [ 'bigInteger', 'decimal', 'integer', 'mediumInteger', 'smallInteger', 'tinyInteger', ]; if (in_array($this->methodName, $availableUnsignedPrefixes)) { return [$this->columnName, 'unsigned'.ucfirst($this->methodName), $this->methodParameters]; } } return [$this->columnName, $this->methodName, $this->methodParameters]; } public function render(): string { [$finalColumnName, $finalMethodName, $finalMethodParameters] = $this->guessLaravelMethod(); $initialString = '$table->'.$finalMethodName.'('; if ($finalColumnName !== null) { $initialString .= ValueToString::make($finalColumnName); } if (count($finalMethodParameters) > 0) { foreach ($finalMethodParameters as $param) { $initialString .= ', '.ValueToString::make($param); } } $initialString .= ')'; if ($this->unsigned && $this->canBeUnsigned($finalMethodName) && ! Str::startsWith($finalMethodName, 'unsigned')) { $initialString .= '->unsigned()'; } if ($this->defaultValue === 'NULL') { $this->defaultValue = null; $this->nullable = true; } if ($this->isNullableMethod($finalMethodName)) { if ($this->nullable === true) { $initialString .= '->nullable()'; } } if ($this->defaultValue !== null) { $initialString .= '->default('; $initialString .= ValueToString::make($this->defaultValue, false); $initialString .= ')'; } if ($this->useCurrent) { $initialString .= '->useCurrent()'; } if ($this->useCurrentOnUpdate) { $initialString .= '->useCurrentOnUpdate()'; } if ($this->index) { $indexName = ''; if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_index_names')) { $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); } $initialString .= '->index('.$indexName.')'; } if ($this->primary && ! $this->isPrimaryKeyMethod($finalMethodName)) { $indexName = ''; if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_primary_key_index_names')) { if ($this->indexDefinitions[0]->getIndexName() !== null) { $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); } } $initialString .= '->primary('.$indexName.')'; } if ($this->unique) { $indexName = ''; if (count($this->indexDefinitions) === 1 && config('laravel-migration-generator.definitions.use_defined_unique_key_index_names')) { $indexName = ValueToString::make($this->indexDefinitions[0]->getIndexName()); } $initialString .= '->unique('.$indexName.')'; } if ($this->storedAs !== null) { $initialString .= '->storedAs('.ValueToString::make($this->storedAs, false, false).')'; } if ($this->virtualAs !== null) { $initialString .= '->virtualAs('.ValueToString::make($this->virtualAs, false, false).')'; } if ($this->comment !== null && config('laravel-migration-generator.definitions.with_comments')) { $initialString .= '->comment('.ValueToString::make($this->comment, false, false).')'; } return $initialString; } } ================================================ FILE: src/Definitions/IndexDefinition.php ================================================ $value) { if (property_exists($this, $attribute)) { $this->$attribute = $value; } } } //region Getters public function getIndexType(): string { return $this->indexType; } public function getIndexName(): ?string { return $this->indexName; } public function getIndexColumns(): array { return $this->indexColumns; } public function getForeignReferencedColumns(): array { return $this->foreignReferencedColumns; } public function getForeignReferencedTable(): string { return $this->foreignReferencedTable; } public function getConstraintActions(): array { return $this->constraintActions; } //endregion //region Setters public function setIndexType(string $indexType): IndexDefinition { $this->indexType = $indexType; return $this; } public function setIndexName(string $indexName): IndexDefinition { $this->indexName = $indexName; return $this; } public function setIndexColumns(array $indexColumns): IndexDefinition { $this->indexColumns = $indexColumns; return $this; } public function setForeignReferencedColumns(array $foreignReferencedColumns): IndexDefinition { $this->foreignReferencedColumns = $foreignReferencedColumns; return $this; } public function setForeignReferencedTable(string $foreignReferencedTable): IndexDefinition { $this->foreignReferencedTable = $foreignReferencedTable; return $this; } public function setConstraintActions(array $constraintActions): IndexDefinition { $this->constraintActions = $constraintActions; return $this; } //endregion public function isMultiColumnIndex() { return count($this->indexColumns) > 1; } /** * Get the escaped index name for safe inclusion in generated PHP code. */ protected function getEscapedIndexName(): string { return ValueToString::escape($this->getIndexName() ?? ''); } public function render(): string { if ($this->indexType === 'foreign') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_foreign_key_index_names')) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } $base = '$table->foreign('.ValueToString::make($this->indexColumns, true).$indexName.')->references('.ValueToString::make($this->foreignReferencedColumns, true).')->on('.ValueToString::make($this->foreignReferencedTable).')'; foreach ($this->constraintActions as $type => $action) { $base .= '->on'.ucfirst($type).'('.ValueToString::make($action).')'; } return $base; } elseif ($this->indexType === 'primary') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_primary_key_index_names') && $this->getIndexName() !== null) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } return '$table->primary('.ValueToString::make($this->indexColumns).$indexName.')'; } elseif ($this->indexType === 'unique') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_unique_key_index_names')) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } return '$table->unique('.ValueToString::make($this->indexColumns).$indexName.')'; } elseif ($this->indexType === 'index') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_index_names')) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } return '$table->index('.ValueToString::make($this->indexColumns).$indexName.')'; } elseif ($this->indexType === 'fulltext') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_index_names')) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } return '$table->fullText('.ValueToString::make($this->indexColumns).$indexName.')'; } elseif ($this->indexType === 'spatial') { $indexName = ''; if (config('laravel-migration-generator.definitions.use_defined_index_names')) { $indexName = ', \''.$this->getEscapedIndexName().'\''; } return '$table->spatialIndex('.ValueToString::make($this->indexColumns).$indexName.')'; } return ''; } } ================================================ FILE: src/Definitions/TableDefinition.php ================================================ */ protected array $columnDefinitions = []; protected array $indexDefinitions = []; public function __construct($attributes = []) { foreach ($attributes as $attribute => $value) { if (property_exists($this, $attribute)) { $this->$attribute = $value; } } } public function getDriver(): string { return $this->driver; } public function getPresentableTableName(): string { if (count($this->getColumnDefinitions()) === 0) { if (count($definitions = $this->getIndexDefinitions()) > 0) { $first = collect($definitions)->first(); //a fk only table from dependency resolution return $this->getTableName().'_'.$first->getIndexName(); } } return $this->getTableName(); } public function getTableName(): string { return $this->tableName; } public function setTableName(string $tableName) { $this->tableName = $tableName; return $this; } public function getColumnDefinitions(): array { return $this->columnDefinitions; } public function setColumnDefinitions(array $columnDefinitions) { $this->columnDefinitions = $columnDefinitions; return $this; } public function addColumnDefinition(ColumnDefinition $definition) { $this->columnDefinitions[] = $definition; return $this; } /** * @return array */ public function getIndexDefinitions(): array { return $this->indexDefinitions; } /** @return array */ public function getForeignKeyDefinitions(): array { return collect($this->getIndexDefinitions())->filter(function ($indexDefinition) { return $indexDefinition->getIndexType() == IndexDefinition::TYPE_FOREIGN; })->toArray(); } public function setIndexDefinitions(array $indexDefinitions) { $this->indexDefinitions = $indexDefinitions; return $this; } public function addIndexDefinition(IndexDefinition $definition) { $this->indexDefinitions[] = $definition; return $this; } public function removeIndexDefinition(IndexDefinition $definition) { foreach ($this->indexDefinitions as $key => $indexDefinition) { if ($definition->getIndexName() == $indexDefinition->getIndexName()) { unset($this->indexDefinitions[$key]); break; } } return $this; } public function getPrimaryKey(): array { return collect($this->getColumnDefinitions()) ->filter(function (ColumnDefinition $columnDefinition) { return $columnDefinition->isPrimary(); })->toArray(); } public function formatter(): TableFormatter { return new TableFormatter($this); } } ================================================ FILE: src/Definitions/ViewDefinition.php ================================================ $value) { if (property_exists($this, $attribute)) { $this->$attribute = $value; } } } public function getDriver(): string { return $this->driver; } public function setDriver(string $driver): ViewDefinition { $this->driver = $driver; return $this; } public function getSchema(): string { return $this->schema; } public function setSchema(string $schema): ViewDefinition { $this->schema = $schema; return $this; } public function getViewName(): string { return $this->viewName; } public function setViewName(string $viewName): ViewDefinition { $this->viewName = $viewName; return $this; } public function formatter(): ViewFormatter { return new ViewFormatter($this); } } ================================================ FILE: src/Formatters/TableFormatter.php ================================================ tableDefinition = $tableDefinition; } public function render($tabCharacter = ' ') { $tableName = $this->tableDefinition->getPresentableTableName(); $schema = $this->getSchema($tabCharacter); $stub = file_get_contents($this->getStubPath()); if (strpos($stub, '[TableUp]') !== false) { //uses new syntax $stub = Formatter::replace($tabCharacter, '[TableUp]', $this->stubTableUp($tabCharacter), $stub); $stub = Formatter::replace($tabCharacter, '[TableDown]', $this->stubTableDown($tabCharacter), $stub); } $stub = str_replace('[TableName:Studly]', Str::studly($tableName), $stub); $stub = str_replace('[TableName]', $tableName, $stub); $stub = Formatter::replace($tabCharacter, '[Schema]', $schema, $stub); return $stub; } public function getStubFileName($index = 0): string { $driver = $this->tableDefinition->getDriver(); $baseStubFileName = ConfigResolver::tableNamingScheme($driver); foreach ($this->stubNameVariables($index) as $variable => $replacement) { if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); } } return $baseStubFileName; } public function getStubPath(): string { $driver = $this->tableDefinition->getDriver(); if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table.stub'))) { return $overridden; } if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table.stub'))) { return $overridden; } return __DIR__.'/../../stubs/table.stub'; } public function getStubCreatePath(): string { $driver = $this->tableDefinition->getDriver(); if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table-create.stub'))) { return $overridden; } if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table-create.stub'))) { return $overridden; } return __DIR__.'/../../stubs/table-create.stub'; } public function getStubModifyPath(): string { $driver = $this->tableDefinition->getDriver(); if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-table-modify.stub'))) { return $overridden; } if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/table-modify.stub'))) { return $overridden; } return __DIR__.'/../../stubs/table-modify.stub'; } public function stubNameVariables($index): array { $tableName = $this->tableDefinition->getPresentableTableName(); return [ 'TableName:Studly' => Str::studly($tableName), 'TableName:Lowercase' => strtolower($tableName), 'TableName' => $tableName, 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), 'Index' => (string) $index, 'IndexedEmptyTimestamp' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 'IndexedTimestamp' => app('laravel-migration-generator:time')->clone()->addSeconds($index)->format('Y_m_d_His'), ]; } public function getSchema($tab = ''): string { $formatter = new Formatter($tab); collect($this->tableDefinition->getColumnDefinitions()) ->filter(fn ($col) => $col->isWritable()) ->each(function ($column) use ($formatter) { $formatter->line($column->render().';'); }); $indices = collect($this->tableDefinition->getIndexDefinitions()) ->filter(fn ($index) => $index->isWritable()); if ($indices->count() > 0) { if (count($this->tableDefinition->getColumnDefinitions()) > 0) { $formatter->line(''); } $indices->each(function ($index) use ($formatter) { $formatter->line($index->render().';'); }); } return $formatter->render(); } public function stubTableUp($tab = '', $variables = null): string { if ($variables === null) { $variables = $this->getStubVariables($tab); } if (count($this->tableDefinition->getColumnDefinitions()) === 0) { $tableModifyStub = file_get_contents($this->getStubModifyPath()); foreach ($variables as $var => $replacement) { $tableModifyStub = Formatter::replace($tab, '['.$var.']', $replacement, $tableModifyStub); } return $tableModifyStub; } $tableUpStub = file_get_contents($this->getStubCreatePath()); foreach ($variables as $var => $replacement) { $tableUpStub = Formatter::replace($tab, '['.$var.']', $replacement, $tableUpStub); } return $tableUpStub; } public function stubTableDown($tab = ''): string { if (count($this->tableDefinition->getColumnDefinitions()) === 0) { $schema = 'Schema::table(\''.$this->tableDefinition->getTableName().'\', function(Blueprint $table){'."\n"; foreach ($this->tableDefinition->getForeignKeyDefinitions() as $indexDefinition) { $schema .= $tab.'$table->dropForeign(\''.$indexDefinition->getIndexName().'\');'."\n"; } return $schema.'});'; } return 'Schema::dropIfExists(\''.$this->tableDefinition->getTableName().'\');'; } protected function getStubVariables($tab = '') { $tableName = $this->tableDefinition->getTableName(); return [ 'TableName:Studly' => Str::studly($tableName), 'TableName:Lowercase' => strtolower($tableName), 'TableName' => $tableName, 'Schema' => $this->getSchema($tab), ]; } public function write(string $basePath, $index = 0, string $tabCharacter = ' '): string { $stub = $this->render($tabCharacter); $fileName = $this->getStubFileName($index); file_put_contents($final = $basePath.'/'.$fileName, $stub); return $final; } } ================================================ FILE: src/Formatters/ViewFormatter.php ================================================ definition = $definition; } public function stubNameVariables($index = 0) { return [ 'ViewName:Studly' => Str::studly($viewName = $this->definition->getViewName()), 'ViewName:Lowercase' => strtolower($viewName), 'ViewName' => $viewName, 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), 'Index' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 'IndexedEmptyTimestamp' => '0000_00_00_'.str_pad((string) $index, 6, '0', STR_PAD_LEFT), 'IndexedTimestamp' => app('laravel-migration-generator:time')->clone()->addSeconds($index)->format('Y_m_d_His'), ]; } protected function getStubFileName($index = 0) { $driver = $this->definition->getDriver(); $baseStubFileName = ConfigResolver::viewNamingScheme($driver); foreach ($this->stubNameVariables($index) as $variable => $replacement) { if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); } } return $baseStubFileName; } protected function getStubPath() { $driver = $this->definition->getDriver(); if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-view.stub'))) { return $overridden; } if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/view.stub'))) { return $overridden; } return __DIR__.'/../../stubs/view.stub'; } public function render($tabCharacter = ' ') { $schema = $this->definition->getSchema(); $stub = file_get_contents($this->getStubPath()); $variables = [ '[ViewName:Studly]' => Str::studly($viewName = $this->definition->getViewName()), '[ViewName]' => $viewName, '[Schema]' => $schema, ]; foreach ($variables as $key => $value) { $stub = Formatter::replace($tabCharacter, $key, $value, $stub); } return $stub; } public function write(string $basePath, $index = 0, string $tabCharacter = ' '): string { $stub = $this->render($tabCharacter); $fileName = $this->getStubFileName($index); file_put_contents($final = $basePath.'/'.$fileName, $stub); return $final; } } ================================================ FILE: src/GeneratorManagers/BaseGeneratorManager.php ================================================ */ public function getTableDefinitions(): array { return $this->tableDefinitions; } /** * @return array */ public function getViewDefinitions(): array { return $this->viewDefinitions; } public function addTableDefinition(TableDefinition $tableDefinition): BaseGeneratorManager { $this->tableDefinitions[] = $tableDefinition; return $this; } public function addViewDefinition(ViewDefinition $definition): BaseGeneratorManager { $this->viewDefinitions[] = $definition; return $this; } public function handle(string $basePath, array $tableNames = [], array $viewNames = []) { $this->init(); $tableDefinitions = collect($this->getTableDefinitions()); $viewDefinitions = collect($this->getViewDefinitions()); $this->createMissingDirectory($basePath); if (count($tableNames) > 0) { $tableDefinitions = $tableDefinitions->filter(function ($tableDefinition) use ($tableNames) { return in_array($tableDefinition->getTableName(), $tableNames); }); } if (count($viewNames) > 0) { $viewDefinitions = $viewDefinitions->filter(function ($viewGenerator) use ($viewNames) { return in_array($viewGenerator->getViewName(), $viewNames); }); } $tableDefinitions = $tableDefinitions->filter(function ($tableDefinition) { return ! $this->skipTable($tableDefinition->getTableName()); }); $viewDefinitions = $viewDefinitions->filter(function ($viewDefinition) { return ! $this->skipView($viewDefinition->getViewName()); }); $sorted = $this->sortTables($tableDefinitions->toArray()); $this->writeTableMigrations($sorted, $basePath); $this->writeViewMigrations($viewDefinitions->toArray(), $basePath, count($sorted)); } /** * @param array $tableDefinitions * @return array */ public function sortTables(array $tableDefinitions): array { if (count($tableDefinitions) <= 1) { return $tableDefinitions; } if (config('laravel-migration-generator.sort_mode') == 'foreign_key') { return (new DependencyResolver($tableDefinitions))->getDependencyOrder(); } return $tableDefinitions; } /** * @param array $tableDefinitions */ public function writeTableMigrations(array $tableDefinitions, $basePath) { foreach ($tableDefinitions as $key => $tableDefinition) { $tableDefinition->formatter()->write($basePath, $key); } } /** * @param array $viewDefinitions */ public function writeViewMigrations(array $viewDefinitions, $basePath, $tableCount = 0) { foreach ($viewDefinitions as $key => $view) { $view->formatter()->write($basePath, $tableCount + $key); } } /** * @return array */ public function skippableTables(): array { return ConfigResolver::skippableTables(static::driver()); } public function skipTable($table): bool { return in_array($table, $this->skippableTables()); } /** * @return array */ public function skippableViews(): array { return ConfigResolver::skippableViews(static::driver()); } public function skipView($view): bool { $skipViews = config('laravel-migration-generator.skip_views'); if ($skipViews) { return true; } return in_array($view, $this->skippableViews()); } } ================================================ FILE: src/GeneratorManagers/Interfaces/GeneratorManagerInterface.php ================================================ $table) { $tableData = (array) $table; $table = $tableData[array_key_first($tableData)]; $tableType = $tableData['Table_type']; if ($tableType === 'BASE TABLE') { $this->addTableDefinition(TableGenerator::init($table)->definition()); } elseif ($tableType === 'VIEW') { $this->addViewDefinition(ViewGenerator::init($table)->definition()); } } } public function addTableDefinition(TableDefinition $tableDefinition): BaseGeneratorManager { $prefix = config('database.connections.'.DB::getDefaultConnection().'.prefix', ''); if (! empty($prefix) && Str::startsWith($tableDefinition->getTableName(), $prefix)) { $tableDefinition->setTableName(Str::replaceFirst($prefix, '', $tableDefinition->getTableName())); } return parent::addTableDefinition($tableDefinition); } } ================================================ FILE: src/Generators/BaseTableGenerator.php ================================================ definition = new TableDefinition([ 'driver' => static::driver(), 'tableName' => $tableName, ]); $this->rows = $rows; } public function definition(): TableDefinition { return $this->definition; } abstract public function resolveStructure(); abstract public function parse(); public static function init(string $tableName, array $rows = []) { $instance = (new static($tableName, $rows)); if ($instance->shouldResolveStructure()) { $instance->resolveStructure(); } $instance->parse(); $instance->cleanUp(); return $instance; } public function shouldResolveStructure(): bool { return count($this->rows) === 0; } public function cleanUp(): void { $this->cleanUpForeignKeyIndices(); $this->cleanUpMorphColumns(); if (! config('laravel-migration-generator.definitions.use_defined_datatype_on_timestamp')) { $this->cleanUpTimestampsColumn(); } $this->cleanUpColumnsWithIndices(); } } ================================================ FILE: src/Generators/BaseViewGenerator.php ================================================ definition = new ViewDefinition([ 'driver' => static::driver(), 'viewName' => $viewName, 'schema' => $schema, ]); } public function definition(): ViewDefinition { return $this->definition; } public static function init(string $viewName, ?string $schema = null) { $obj = new static($viewName, $schema); if ($schema === null) { $obj->resolveSchema(); } $obj->parse(); return $obj; } } ================================================ FILE: src/Generators/Concerns/CleansUpColumnIndices.php ================================================ definition()->getIndexDefinitions() as &$index) { /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $index */ if (! $index->isWritable()) { continue; } $columns = $index->getIndexColumns(); foreach ($columns as $indexColumn) { foreach ($this->definition()->getColumnDefinitions() as $column) { if ($column->getColumnName() === $indexColumn) { $indexType = $index->getIndexType(); $isMultiColumnIndex = $index->isMultiColumnIndex(); if ($indexType === 'primary' && ! $isMultiColumnIndex) { $column->setPrimary(true)->addIndexDefinition($index); $index->markAsWritable(false); } elseif ($indexType === 'index' && ! $isMultiColumnIndex) { $isForeignKeyIndex = false; foreach ($this->definition()->getIndexDefinitions() as $innerIndex) { $innerIndexColumns = $innerIndex->getIndexColumns(); if ($innerIndex->getIndexType() === 'foreign' && ! $innerIndex->isMultiColumnIndex() && ! empty($innerIndexColumns) && $innerIndexColumns[0] == $column->getColumnName()) { $isForeignKeyIndex = true; break; } } if ($isForeignKeyIndex === false) { $column->setIndex(true)->addIndexDefinition($index); } $index->markAsWritable(false); } elseif ($indexType === 'unique' && ! $isMultiColumnIndex) { $column->setUnique(true)->addIndexDefinition($index); $index->markAsWritable(false); } } } } } } } ================================================ FILE: src/Generators/Concerns/CleansUpForeignKeyIndices.php ================================================ definition()->getIndexDefinitions(); foreach ($indexDefinitions as $index) { /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $index */ if ($index->getIndexType() === 'index') { //look for corresponding foreign key for this index $columns = $index->getIndexColumns(); $indexName = $index->getIndexName(); foreach ($indexDefinitions as $innerIndex) { /** @var \LaravelMigrationGenerator\Definitions\IndexDefinition $innerIndex */ if ($innerIndex->getIndexName() !== $indexName) { if ($innerIndex->getIndexType() === 'foreign') { $cols = $innerIndex->getIndexColumns(); if (count(array_intersect($columns, $cols)) === count($columns)) { //has same columns $index->markAsWritable(false); break; } } } } } } } } ================================================ FILE: src/Generators/Concerns/CleansUpMorphColumns.php ================================================ definition()->getColumnDefinitions() as &$column) { if (Str::endsWith($columnName = $column->getColumnName(), ['_id', '_type'])) { $pieces = explode('_', $columnName); $type = array_pop($pieces); //pop off id or type $morphColumn = implode('_', $pieces); $morphColumns[$morphColumn][$type] = $column; } } foreach ($morphColumns as $columnName => $fields) { if (count($fields) === 2) { /** @var ColumnDefinition $idField */ $idField = $fields['id']; /** @var ColumnDefinition $typeField */ $typeField = $fields['type']; if (! ($idField->isUUID() || Str::contains($idField->getMethodName(), 'integer'))) { //should only be a uuid field or integer continue; } if ($typeField->getMethodName() != 'string') { //should only be a string field continue; } if ($idField->isUUID()) { //UUID morph $idField ->setMethodName('uuidMorphs') ->setMethodParameters([]) ->setColumnName($columnName); } else { //regular morph $idField ->setMethodName('morphs') ->setColumnName($columnName); } $typeField->markAsWritable(false); foreach ($this->definition->getIndexDefinitions() as $index) { $columns = $index->getIndexColumns(); $morphColumns = [$columnName.'_id', $columnName.'_type']; if (count($columns) == count($morphColumns) && array_diff($columns, $morphColumns) === array_diff($morphColumns, $columns)) { $index->markAsWritable(false); break; } } } } } } ================================================ FILE: src/Generators/Concerns/CleansUpTimestampsColumn.php ================================================ definition()->getColumnDefinitions() as &$column) { $columnName = $column->getColumnName(); if ($columnName === 'created_at') { $timestampColumns['created_at'] = $column; } elseif ($columnName === 'updated_at') { $timestampColumns['updated_at'] = $column; } if (count($timestampColumns) === 2) { foreach ($timestampColumns as $timestampColumn) { if ($timestampColumn->useCurrent() || $timestampColumn->useCurrentOnUpdate()) { //don't convert to a `timestamps()` method if useCurrent is used return; } } $timestampColumns['created_at'] ->setColumnName(null) ->setMethodName('timestamps') ->setNullable(false); $timestampColumns['updated_at']->markAsWritable(false); break; } } } } ================================================ FILE: src/Generators/Concerns/WritesToFile.php ================================================ isWritable()) { return; } $stub = $this->generateStub($tabCharacter); $fileName = $this->getStubFileName($index); file_put_contents($basePath.'/'.$fileName, $stub); } } ================================================ FILE: src/Generators/Concerns/WritesViewsToFile.php ================================================ Str::studly($this->viewName), 'ViewName:Lowercase' => strtolower($this->viewName), 'ViewName' => $this->viewName, 'Timestamp' => app('laravel-migration-generator:time')->format('Y_m_d_His'), ]; } protected function getStubFileName() { $driver = static::driver(); $baseStubFileName = ConfigResolver::viewNamingScheme($driver); foreach ($this->stubNameVariables() as $variable => $replacement) { if (preg_match("/\[".$variable."\]/i", $baseStubFileName) === 1) { $baseStubFileName = preg_replace("/\[".$variable."\]/i", $replacement, $baseStubFileName); } } return $baseStubFileName; } protected function getStubPath() { $driver = static::driver(); if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/'.$driver.'-view.stub'))) { return $overridden; } if (file_exists($overridden = resource_path('stubs/vendor/laravel-migration-generator/view.stub'))) { return $overridden; } return __DIR__.'/../../../stubs/view.stub'; } protected function generateStub($tabCharacter = ' ') { $tab = str_repeat($tabCharacter, 3); $schema = $this->getSchema(); $stub = file_get_contents($this->getStubPath()); $stub = str_replace('[ViewName:Studly]', Str::studly($this->viewName), $stub); $stub = str_replace('[ViewName]', $this->viewName, $stub); $stub = str_replace('[Schema]', $tab.$schema, $stub); return $stub; } } ================================================ FILE: src/Generators/Interfaces/TableGeneratorInterface.php ================================================ definition()->getTableName().'`'); $structure = $structure[0]; $structure = (array) $structure; if (isset($structure['Create Table'])) { $lines = explode("\n", $structure['Create Table']); array_shift($lines); //get rid of first line array_pop($lines); //get rid of last line $lines = array_map(fn ($item) => trim($item), $lines); $this->rows = $lines; } else { $this->rows = []; } } protected function isColumnLine($line) { return ! Str::startsWith($line, ['KEY', 'PRIMARY', 'UNIQUE', 'FULLTEXT', 'CONSTRAINT']); } public function parse() { foreach ($this->rows as $line) { if ($this->isColumnLine($line)) { $tokenizer = ColumnTokenizer::parse($line); $this->definition()->addColumnDefinition($tokenizer->definition()); } else { $tokenizer = IndexTokenizer::parse($line); $this->definition()->addIndexDefinition($tokenizer->definition()); } } } } ================================================ FILE: src/Generators/MySQL/ViewGenerator.php ================================================ definition()->getViewName().'`'); $structure = $structure[0]; $structure = (array) $structure; if (isset($structure['Create View'])) { $this->definition()->setSchema($structure['Create View']); } } public function parse() { $schema = $this->definition()->getSchema(); if (preg_match('/CREATE(.*?)VIEW/', $schema, $matches)) { $schema = str_replace($matches[1], ' ', $schema); } if (preg_match_all('/isnull\((.+?)\)/', $schema, $matches)) { foreach ($matches[0] as $key => $match) { $schema = str_replace($match, $matches[1][$key].' IS NULL', $schema); } } if (preg_match('/collate utf8mb4_unicode_ci/', $schema)) { $schema = str_replace('collate utf8mb4_unicode_ci', '', $schema); } $this->definition()->setSchema($schema); } } ================================================ FILE: src/Helpers/ConfigResolver.php ================================================ */ protected array $tableDefinitions = []; /** @var array */ protected array $sorted = []; public function __construct(array $tableDefinitions) { $this->tableDefinitions = $tableDefinitions; $this->build(); } protected function build() { /** @var TableDefinition[] $keyedDefinitions */ $keyedDefinitions = collect($this->tableDefinitions) ->keyBy(function (TableDefinition $tableDefinition) { return $tableDefinition->getTableName(); }); $dependencies = []; foreach ($this->tableDefinitions as $tableDefinition) { $dependencies[$tableDefinition->getTableName()] = []; } foreach ($this->tableDefinitions as $tableDefinition) { foreach ($tableDefinition->getForeignKeyDefinitions() as $indexDefinition) { $referencedTable = $indexDefinition->getForeignReferencedTable(); if (empty($referencedTable)) { continue; } if ($referencedTable === $tableDefinition->getTableName()) { continue; } if (! in_array($referencedTable, $dependencies[$tableDefinition->getTableName()])) { $dependencies[$tableDefinition->getTableName()][] = $referencedTable; } } } $sorter = new FixedArraySort(); $circulars = []; $sorter->setCircularInterceptor(function ($nodes) use (&$circulars) { $circulars[] = [$nodes[count($nodes) - 2], $nodes[count($nodes) - 1]]; }); foreach ($dependencies as $table => $dependencyArray) { $sorter->add($table, $dependencyArray); } $sorted = $sorter->sort(); $definitions = collect($sorted)->map(function ($item) use ($keyedDefinitions) { return $keyedDefinitions[$item]; })->toArray(); foreach ($circulars as $groups) { [$start, $end] = $groups; $startDefinition = $keyedDefinitions[$start]; $indicesForStart = collect($startDefinition->getForeignKeyDefinitions()) ->filter(function (IndexDefinition $index) use ($end) { return $index->getForeignReferencedTable() == $end; }); foreach ($indicesForStart as $index) { $startDefinition->removeIndexDefinition($index); } if (! in_array($start, $sorted)) { $definitions[] = $startDefinition; } $endDefinition = $keyedDefinitions[$end]; $indicesForEnd = collect($endDefinition->getForeignKeyDefinitions()) ->filter(function (IndexDefinition $index) use ($start) { return $index->getForeignReferencedTable() == $start; }); foreach ($indicesForEnd as $index) { $endDefinition->removeIndexDefinition($index); } if (! in_array($end, $sorted)) { $definitions[] = $endDefinition; } $definitions[] = new TableDefinition([ 'tableName' => $startDefinition->getTableName(), 'driver' => $startDefinition->getDriver(), 'columnDefinitions' => [], 'indexDefinitions' => $indicesForStart->toArray(), ]); $definitions[] = new TableDefinition([ 'tableName' => $endDefinition->getTableName(), 'driver' => $endDefinition->getDriver(), 'columnDefinitions' => [], 'indexDefinitions' => $indicesForEnd->toArray(), ]); } $this->sorted = $definitions; } /** * @return TableDefinition[] */ public function getDependencyOrder(): array { return $this->sorted; } } ================================================ FILE: src/Helpers/Formatter.php ================================================ tabCharacter = $tabCharacter; } public function line(string $data, $indentTimes = 0) { $this->lines[] = str_repeat($this->tabCharacter, $indentTimes).$data; return function ($data) use ($indentTimes) { return $this->line($data, $indentTimes + 1); }; } public function render($extraIndent = 0) { $lines = $this->lines; if ($extraIndent > 0) { $lines = collect($lines)->map(function ($item, $index) use ($extraIndent) { if ($index === 0) { return $item; } return str_repeat($this->tabCharacter, $extraIndent).$item; })->toArray(); } return implode("\n", $lines); } public function replaceOnLine($toReplace, $body) { if (preg_match('/^(\s+)?'.preg_quote($toReplace).'/m', $body, $matches) !== false) { $gap = $matches[1] ?? ''; $numSpaces = strlen($this->tabCharacter); if ($numSpaces === 0) { $startingTabIndent = 0; } else { $startingTabIndent = (int) (strlen($gap) / $numSpaces); } return preg_replace('/'.preg_quote($toReplace).'/', $this->render($startingTabIndent), $body); } return $body; } public static function replace($tabCharacter, $toReplace, $replacement, $body) { $formatter = new static($tabCharacter); foreach (explode("\n", $replacement) as $line) { $formatter->line($line); } return $formatter->replaceOnLine($toReplace, $body); } } ================================================ FILE: src/Helpers/ValueToString.php ================================================ map(fn ($item) => $quote.static::escape($item, $singleQuote).$quote)->implode(', ').']'; } elseif (is_int($value) || is_float($value)) { return $value; } if (static::isCastedValue($value)) { return static::parseCastedValue($value); } if (Str::startsWith($value, $quote) && Str::endsWith($value, $quote)) { return $value; } return $quote.static::escape($value, $singleQuote).$quote; } } ================================================ FILE: src/Helpers/WritableTrait.php ================================================ writable = $writable; return $this; } public function isWritable() { return $this->writable; } } ================================================ FILE: src/LaravelMigrationGeneratorProvider.php ================================================ mergeConfigFrom( __DIR__.'/../config/laravel-migration-generator.php', 'laravel-migration-generator' ); $this->publishes([ __DIR__.'/../stubs' => resource_path('stubs/vendor/laravel-migration-generator'), __DIR__.'/../config/laravel-migration-generator.php' => config_path('laravel-migration-generator.php'), ]); if ($this->app->runningInConsole()) { $this->app->instance('laravel-migration-generator:time', now()); $this->commands([ GenerateMigrationsCommand::class, ]); } if (config('laravel-migration-generator.run_after_migrations') && config('app.env') === 'local') { Event::listen(MigrationsEnded::class, function () { Artisan::call('generate:migrations'); }); } } } ================================================ FILE: src/Tokenizers/BaseColumnTokenizer.php ================================================ definition = new ColumnDefinition(); parent::__construct($value); } public function definition(): ColumnDefinition { return $this->definition; } } ================================================ FILE: src/Tokenizers/BaseIndexTokenizer.php ================================================ definition = new IndexDefinition(); parent::__construct($value); } public function definition(): IndexDefinition { return $this->definition; } } ================================================ FILE: src/Tokenizers/BaseTokenizer.php ================================================ value = $value; $prune = false; $pruneSingleQuotes = false; if (preg_match("/(DEFAULT|COMMENT) ''/", $value, $matches)) { $value = str_replace($matches[1].' \'\'', $matches[1].' '.self::EMPTY_STRING_REPLACER, $value); } //first get rid of any single quoted stuff with '' around it if (preg_match_all('/\'\'(.+?)\'\'/', $value, $matches)) { foreach ($matches[0] as $key => $singleQuoted) { $toReplace = $singleQuoted; $value = str_replace($toReplace, self::SINGLE_QUOTE_REPLACER.$matches[1][$key].self::SINGLE_QUOTE_REPLACER, $value); $pruneSingleQuotes = true; } } if (preg_match_all("/'(.*?)'/", $value, $matches)) { foreach ($matches[0] as $quoteWithSpace) { //we've got an enum or set that has spaces in the text //so we'll convert to a different character so it doesn't get pruned $toReplace = $quoteWithSpace; $value = str_replace($toReplace, str_replace(' ', self::SPACE_REPLACER, $toReplace), $value); $prune = true; } } $value = str_replace(self::EMPTY_STRING_REPLACER, '\'\'', $value); $this->tokens = array_map(function ($item) { return trim($item, ', '); }, str_getcsv($value, ' ', "'")); if ($prune) { $this->tokens = array_map(function ($item) { return str_replace(self::SPACE_REPLACER, ' ', $item); }, $this->tokens); } if ($pruneSingleQuotes) { $this->tokens = array_map(function ($item) { return str_replace(self::SINGLE_QUOTE_REPLACER, '\'', $item); }, $this->tokens); } } public static function make(string $line) { return new static($line); } /** * @return static */ public static function parse(string $line) { return (new static($line))->tokenize(); } protected function parseColumn($value) { return trim($value, '` '); } protected function columnsToArray($string) { $string = trim($string, '()'); return array_map(fn ($item) => $this->parseColumn($item), explode(',', $string)); } protected function consume() { return array_shift($this->tokens); } protected function putBack($value) { array_unshift($this->tokens, $value); } } ================================================ FILE: src/Tokenizers/Interfaces/ColumnTokenizerInterface.php ================================================ consumeColumnName(); $this->consumeColumnType(); if ($this->isNumberType()) { $this->consumeUnsigned(); $this->consumeZeroFill(); } if ($this->isTextType()) { //possibly has a character set $this->consumeCharacterSet(); //has collation data most likely $this->consumeCollation(); } $this->consumeNullable(); $this->consumeDefaultValue(); if ($this->isNumberType()) { $this->consumeAutoIncrement(); $this->consumeKeyConstraints(); } $this->consumeGenerated(); if ($this->columnDataType == 'timestamp' || $this->columnDataType == 'datetime') { $this->consumeTimestamp(); } $this->consumeComment(); return $this; } //region Consumers protected function consumeColumnName() { $this->definition->setColumnName($this->parseColumn($this->consume())); } protected function consumeZeroFill() { $nextPiece = $this->consume(); if (strtoupper($nextPiece) === 'ZEROFILL') { $this->zeroFill = true; } else { $this->putBack($nextPiece); } } protected function consumeColumnType() { $originalColumnType = $columnType = $this->consume(); $hasConstraints = Str::contains($columnType, '('); if ($hasConstraints) { $columnType = explode('(', $columnType)[0]; } $this->columnDataType = strtolower($columnType); $this->resolveColumnMethod(); if ($hasConstraints) { preg_match("/\((.+?)\)/", $originalColumnType, $constraintMatches); $matches = explode(',', $constraintMatches[1]); $this->resolveColumnConstraints($matches); } } private function consumeAutoIncrement() { $piece = $this->consume(); if (strtoupper($piece) === 'AUTO_INCREMENT') { $this->definition->setAutoIncrementing(true); } else { $this->putBack($piece); } } protected function consumeNullable() { $piece = $this->consume(); if (strtoupper($piece) === 'NOT') { $this->consume(); //next is NULL $this->definition->setNullable(false); } elseif (strtoupper($piece) === 'NULL') { $this->definition->setNullable(true); } else { //something else $this->putBack($piece); } if (Str::contains($this->columnDataType, 'text')) { //text column types are explicitly nullable unless set to NOT NULL if ($this->definition->isNullable() === null) { $this->definition->setNullable(true); } } } protected function consumeDefaultValue() { $piece = $this->consume(); if (strtoupper($piece) === 'DEFAULT') { $this->definition->setDefaultValue($this->consume()); if (strtoupper($this->definition->getDefaultValue()) === 'NULL') { $this->definition ->setNullable(true) ->setDefaultValue(null); } elseif (strtoupper($this->definition->getDefaultValue()) === 'CURRENT_TIMESTAMP') { $this->definition ->setDefaultValue(null) ->setUseCurrent(true); } elseif (preg_match("/b'([01]+)'/i", $this->definition->getDefaultValue(), $matches)) { // Binary digit, so let's convert to PHP's version $this->definition->setDefaultValue(ValueToString::castBinary($matches[1])); } if ($this->definition->getDefaultValue() !== null) { if ($this->isNumberType()) { if (Str::contains(strtoupper($this->columnDataType), 'INT')) { $this->definition->setDefaultValue((int) $this->definition->getDefaultValue()); } else { //floats get converted to strings improperly, gotta do a string cast $this->definition->setDefaultValue(ValueToString::castFloat($this->definition->getDefaultValue())); } } else { if (! $this->isBinaryType()) { $this->definition->setDefaultValue((string) $this->definition->getDefaultValue()); } } } } else { $this->putBack($piece); } } protected function consumeComment() { $piece = $this->consume(); if (strtoupper($piece) === 'COMMENT') { // next piece is the comment content $this->definition->setComment($this->consume()); } else { $this->putBack($piece); } } protected function consumeCharacterSet() { $piece = $this->consume(); if (strtoupper($piece) === 'CHARACTER') { $this->consume(); // SET, throw it away $this->definition->setCharacterSet($this->consume()); } else { //something else $this->putBack($piece); } } protected function consumeCollation() { $piece = $this->consume(); if (strtoupper($piece) === 'COLLATE') { //next piece is the collation type $this->definition->setCollation($this->consume()); } else { $this->putBack($piece); } } private function consumeUnsigned() { $piece = $this->consume(); if (strtoupper($piece) === 'UNSIGNED') { $this->definition->setUnsigned(true); } else { $this->putBack($piece); } } private function consumeKeyConstraints() { $nextPiece = $this->consume(); if (strtoupper($nextPiece) === 'PRIMARY') { $this->definition->setPrimary(true); $next = $this->consume(); if (strtoupper($next) !== 'KEY') { $this->putBack($next); } } elseif (strtoupper($nextPiece) === 'UNIQUE') { $this->definition->setUnique(true); $next = $this->consume(); if (strtoupper($next) !== 'KEY') { $this->putBack($next); } } else { $this->putBack($nextPiece); } } private function consumeGenerated() { $canContinue = false; $nextPiece = $this->consume(); if (strtoupper($nextPiece) === 'GENERATED') { $piece = $this->consume(); if (strtoupper($piece) === 'ALWAYS') { $this->consume(); // AS $canContinue = true; } else { $this->putBack($piece); } } elseif (strtoupper($nextPiece) === 'AS') { $canContinue = true; } if (! $canContinue) { $this->putBack($nextPiece); return; } $expressionPieces = []; $parenthesisCounter = 0; while ($pieceOfExpression = $this->consume()) { $numOpeningParenthesis = substr_count($pieceOfExpression, '('); $numClosingParenthesis = substr_count($pieceOfExpression, ')'); $parenthesisCounter += $numOpeningParenthesis - $numClosingParenthesis; $expressionPieces[] = $pieceOfExpression; if ($parenthesisCounter === 0) { break; } } $expression = implode(' ', $expressionPieces); if (Str::startsWith($expression, '((') && Str::endsWith($expression, '))')) { $expression = substr($expression, 1, strlen($expression) - 2); } $finalPiece = $this->consume(); if ($finalPiece !== null && strtoupper($finalPiece) === 'STORED') { $this->definition->setStoredAs($expression)->setNullable(false); } else { $this->definition->setVirtualAs($expression)->setNullable(false); } } private function consumeTimestamp() { $nextPiece = $this->consume(); if (strtoupper($nextPiece) === 'ON') { $next = $this->consume(); if (strtoupper($next) === 'UPDATE') { $next = $this->consume(); if (strtoupper($next) === 'CURRENT_TIMESTAMP') { $this->definition->setUseCurrentOnUpdate(true); } else { $this->putBack($next); } } else { $this->putBack($next); } } else { $this->putBack($nextPiece); } } //endregion //region Resolvers private function resolveColumnMethod() { $mapped = [ 'int' => 'integer', 'tinyint' => 'tinyInteger', 'smallint' => 'smallInteger', 'mediumint' => 'mediumInteger', 'bigint' => 'bigInteger', 'varchar' => 'string', 'tinytext' => 'string', //tinytext is not a valid Blueprint method currently 'mediumtext' => 'mediumText', 'longtext' => 'longText', 'blob' => 'binary', 'datetime' => 'dateTime', 'geometrycollection' => 'geometryCollection', 'linestring' => 'lineString', 'multilinestring' => 'multiLineString', 'multipolygon' => 'multiPolygon', 'multipoint' => 'multiPoint', ]; if (isset($mapped[$this->columnDataType])) { $this->definition->setMethodName($mapped[$this->columnDataType]); } else { //do some custom resolution $this->definition->setMethodName($this->columnDataType); } } private function resolveColumnConstraints(array $constraints) { if ($this->columnDataType === 'char' && count($constraints) === 1 && $constraints[0] == 36) { //uuid for mysql $this->definition->setIsUUID(true); return; } if ($this->isArrayType()) { $this->definition->setMethodParameters([array_map(fn ($item) => trim($item, '\''), $constraints)]); } else { if (Str::contains(strtoupper($this->columnDataType), 'INT')) { $this->definition->setMethodParameters([]); //laravel does not like display field widths } else { if ($this->definition->getMethodName() === 'string') { if (count($constraints) === 1) { //has a width set if ($constraints[0] == Builder::$defaultStringLength) { $this->definition->setMethodParameters([]); return; } } } $this->definition->setMethodParameters(array_map(fn ($item) => (int) $item, $constraints)); } } } //endregion protected function isTextType() { return Str::contains($this->columnDataType, ['char', 'text', 'set', 'enum']); } protected function isNumberType() { return Str::contains($this->columnDataType, ['int', 'decimal', 'float', 'double']); } protected function isArrayType() { return Str::contains($this->columnDataType, ['enum', 'set']); } protected function isBinaryType() { return Str::contains($this->columnDataType, ['bit']); } /** * @return mixed */ public function getColumnDataType() { return $this->columnDataType; } } ================================================ FILE: src/Tokenizers/MySQL/IndexTokenizer.php ================================================ consumeIndexType(); if ($this->definition->getIndexType() !== 'primary') { $this->consumeIndexName(); } if ($this->definition->getIndexType() === 'foreign') { $this->consumeForeignKey(); } else { $this->consumeIndexColumns(); } return $this; } private function consumeIndexType() { $piece = $this->consume(); $upper = strtoupper($piece); if (in_array($upper, ['PRIMARY', 'UNIQUE', 'FULLTEXT', 'SPATIAL'])) { $this->definition->setIndexType(strtolower($piece)); $this->consume(); //just the word KEY } elseif ($upper === 'KEY') { $this->definition->setIndexType('index'); } elseif ($upper === 'CONSTRAINT') { $this->definition->setIndexType('foreign'); } } private function consumeIndexName() { $piece = $this->consume(); $this->definition->setIndexName($this->parseColumn($piece)); } private function consumeIndexColumns() { $piece = $this->consume(); $columns = $this->columnsToArray($piece); $this->definition->setIndexColumns($columns); } private function consumeForeignKey() { $piece = $this->consume(); if (strtoupper($piece) === 'FOREIGN') { $this->consume(); //KEY $columns = []; $token = $this->consume(); while (! is_null($token)) { $columns = array_merge($columns, $this->columnsToArray($token)); $token = $this->consume(); if (strtoupper($token) === 'REFERENCES') { $this->putBack($token); break; } } $this->definition->setIndexColumns($columns); $this->consume(); //REFERENCES $referencedTable = $this->parseColumn($this->consume()); $this->definition->setForeignReferencedTable($referencedTable); $referencedColumns = []; $token = $this->consume(); while (! is_null($token)) { $referencedColumns = array_merge($referencedColumns, $this->columnsToArray($token)); $token = $this->consume(); if (strtoupper($token) === 'ON') { $this->putBack($token); break; } } $this->definition->setForeignReferencedColumns($referencedColumns); $this->consumeConstraintActions(); } else { $this->putBack($piece); } } private function consumeConstraintActions() { while ($token = $this->consume()) { if (strtoupper($token) === 'ON') { $actionType = strtolower($this->consume()); //UPDATE $actionMethod = strtolower($this->consume()); //CASCADE | NO ACTION | SET NULL | SET DEFAULT if ($actionMethod === 'no') { $this->consume(); //consume ACTION $actionMethod = 'restrict'; } elseif ($actionMethod === 'set') { $actionMethod = 'set '.$this->consume(); //consume NULL or DEFAULT } $currentActions = $this->definition->getConstraintActions(); $currentActions[$actionType] = $actionMethod; $this->definition->setConstraintActions($currentActions); } else { $this->putBack($token); break; } } } } ================================================ FILE: stubs/table-create.stub ================================================ Schema::create('[TableName]', function (Blueprint $table) { [Schema] }); ================================================ FILE: stubs/table-modify.stub ================================================ Schema::table('[TableName]', function (Blueprint $table) { [Schema] }); ================================================ FILE: stubs/table.stub ================================================ dropView()); DB::statement($this->createView()); } /** * Reverse the migrations. * * @return void */ public function down() { DB::statement($this->dropView()); } private function createView() { return <<set('database.default', 'testbench'); $app['config']->set('database.connections.testbench', [ 'driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '', ]); } protected function getPackageProviders($app) { return [ LaravelMigrationGeneratorProvider::class, ]; } } ================================================ FILE: tests/Unit/ColumnDefinitionTest.php ================================================ setIndex(true)->setColumnName('testing')->setMethodName('string'); $indexDefinition = (new IndexDefinition())->setIndexName('test')->setIndexType('index'); $columnDefinition->addIndexDefinition($indexDefinition); $this->assertEquals('$table->string(\'testing\')->index(\'test\')', $columnDefinition->render()); } public function test_it_prunes_empty_primary_key_index() { $columnDefinition = (new ColumnDefinition()) ->setPrimary(true) ->setColumnName('testing') ->setUnsigned(true) ->setMethodName('integer'); $indexDefinition = (new IndexDefinition()) ->setIndexType('primary'); $columnDefinition->addIndexDefinition($indexDefinition); $this->assertEquals('$table->unsignedInteger(\'testing\')->primary()', $columnDefinition->render()); } } ================================================ FILE: tests/Unit/DependencyResolverTest.php ================================================ 'tests', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('name')->setMethodName('string')->setNullable(false), ], 'indexDefinitions' => [], ]); $foreignTableDefinition = new TableDefinition([ 'tableName' => 'test_items', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_test_id')->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), ], ]); $resolver = new DependencyResolver([$tableDefinition, $foreignTableDefinition]); $order = $resolver->getDependencyOrder(); $this->assertCount(2, $order); $this->assertEquals('tests', $order[0]->getTableName()); $this->assertEquals('test_items', $order[1]->getTableName()); } public function test_it_finds_cyclical_dependencies() { $tableDefinition = new TableDefinition([ 'tableName' => 'tests', 'driver' => 'mysql', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('test_item_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_test_item_id')->setIndexColumns(['test_item_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('test_items'), ], ]); $foreignTableDefinition = new TableDefinition([ 'tableName' => 'test_items', 'driver' => 'mysql', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_test_id')->setIndexColumns(['test_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), ], ]); $resolver = new DependencyResolver([$tableDefinition, $foreignTableDefinition]); $order = $resolver->getDependencyOrder(); $this->assertCount(4, $order); $this->assertEquals('$table->foreign(\'test_id\', \'fk_test_id\')->references(\'id\')->on(\'tests\');', $order[2]->formatter()->getSchema()); $this->assertEquals('$table->foreign(\'test_item_id\', \'fk_test_item_id\')->references(\'id\')->on(\'test_items\');', $order[3]->formatter()->getSchema()); } public function test_it_finds_self_referential_dependencies() { $tableDefinition = new TableDefinition([ 'tableName' => 'tests', 'driver' => 'mysql', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('parent_id')->setMethodName('integer')->setUnsigned(true)->setNullable(false), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_parent_id')->setIndexColumns(['parent_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), ], ]); $resolver = new DependencyResolver([$tableDefinition]); $order = $resolver->getDependencyOrder(); $this->assertCount(1, $order); $this->assertCount(2, $order[0]->getColumnDefinitions()); $this->assertCount(1, $order[0]->getIndexDefinitions()); } } ================================================ FILE: tests/Unit/FormatterTest.php ================================================ line('Test'); $this->assertEquals('Test', $formatter->render()); } public function test_can_chain() { $formatter = new Formatter(); $line = $formatter->line('$this->call(function(){'); $line('$this->die();'); $formatter->line('});'); $this->assertEquals(<<<'STR' $this->call(function(){ $this->die(); }); STR, $formatter->render()); } public function test_can_get_current_line_indent_level() { $formatter = new Formatter(); $formatter->line('Line'); $formatter->line('Line 2'); $body = <<<'STR' [Test] STR; $replaced = $formatter->replaceOnLine('[Test]', $body); $shouldEqual = <<<'STR' Line Line 2 STR; $this->assertEquals($shouldEqual, $replaced); } public function test_can_replace_on_no_indent() { $replaced = Formatter::replace(' ', '[TEST]', 'Test', '[TEST]'); $this->assertEquals('Test', $replaced); } } ================================================ FILE: tests/Unit/GeneratorManagers/MySQLGeneratorManagerTest.php ================================================ partialMock(MySQLGeneratorManager::class, function (MockInterface $mock) use ($tableDefinitions) { $mock->shouldReceive('init', 'createMissingDirectory', 'writeTableMigrations', 'writeViewMigrations'); $mock->shouldReceive('createMissingDirectory'); $mock->shouldReceive('getTableDefinitions')->andReturn($tableDefinitions); }); } public function test_can_sort_tables() { /** @var MySQLGeneratorManager $mocked */ $mocked = $this->getManagerMock([ new TableDefinition([ 'tableName' => 'tests', 'driver' => 'mysql', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('test_item_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_test_item_id')->setIndexColumns(['test_item_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('test_items'), ], ]), new TableDefinition([ 'tableName' => 'test_items', 'driver' => 'mysql', 'columnDefinitions' => [ (new ColumnDefinition())->setColumnName('id')->setMethodName('id')->setAutoIncrementing(true)->setPrimary(true), (new ColumnDefinition())->setColumnName('test_id')->setMethodName('bigInteger')->setNullable(false)->setUnsigned(true), ], 'indexDefinitions' => [ (new IndexDefinition())->setIndexName('fk_test_id')->setIndexColumns(['test_id'])->setIndexType('foreign')->setForeignReferencedColumns(['id'])->setForeignReferencedTable('tests'), ], ]), ]); $sorted = $mocked->sortTables($mocked->getTableDefinitions()); $this->assertCount(4, $sorted); $this->assertStringContainsString('$table->dropForeign', $sorted[3]->formatter()->stubTableDown()); } public function test_can_remove_database_prefix() { $connection = DB::getDefaultConnection(); config()->set('database.connections.'.$connection.'.prefix', 'wp_'); $mocked = $this->partialMock(MySQLGeneratorManager::class, function (MockInterface $mock) { $mock->shouldReceive('init'); }); $definition = (new TableDefinition())->setTableName('wp_posts'); $mocked->addTableDefinition($definition); $this->assertEquals('posts', $definition->getTableName()); $definition = (new TableDefinition())->setTableName('posts'); $mocked->addTableDefinition($definition); $this->assertEquals('posts', $definition->getTableName()); config()->set('database.connections.'.$connection.'.prefix', ''); $definition = (new TableDefinition())->setTableName('wp_posts'); $mocked->addTableDefinition($definition); $this->assertEquals('wp_posts', $definition->getTableName()); $definition = (new TableDefinition())->setTableName('posts'); $mocked->addTableDefinition($definition); $this->assertEquals('posts', $definition->getTableName()); } } ================================================ FILE: tests/Unit/Generators/MySQLTableGeneratorTest.php ================================================ cleanUpMigrations($path); } private function assertSchemaHas($str, $schema) { $this->assertStringContainsString($str, $schema); } public function test_runs_correctly() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` int(9) unsigned NOT NULL', '`note` varchar(255) NOT NULL', 'KEY `fk_user_id_idx` (`user_id`)', 'CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->increments(\'id\');', $schema); $this->assertSchemaHas('$table->unsignedInteger(\'user_id\');', $schema); $this->assertSchemaHas('$table->string(\'note\');', $schema); $this->assertSchemaHas('$table->foreign(\'user_id\', \'fk_user_id\')->references(\'id\')->on(\'users\')->onDelete(\'cascade\')->onUpdate(\'cascade\');', $schema); } public function test_self_referential_foreign_key() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`parent_id` int(9) unsigned NOT NULL', 'KEY `fk_parent_id_idx` (`parent_id`)', 'CONSTRAINT `fk_parent_id` FOREIGN KEY (`parent_id`) REFERENCES `tables` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->increments(\'id\');', $schema); $this->assertSchemaHas('$table->unsignedInteger(\'parent_id\');', $schema); $this->assertSchemaHas('$table->foreign(\'parent_id\', \'fk_parent_id\')->references(\'id\')->on(\'tables\')->onDelete(\'cascade\')->onUpdate(\'cascade\');', $schema); } private function cleanUpMigrations($path) { if (is_dir($path)) { foreach (glob($path.'/*.php') as $file) { unlink($file); } rmdir($path); } } public function test_writes() { Config::set('laravel-migration-generator.table_naming_scheme', '0000_00_00_000000_create_[TableName]_table.php'); $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` int(9) unsigned NOT NULL', '`note` varchar(255) NOT NULL', 'KEY `fk_user_id_idx` (`user_id`)', 'CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE ON UPDATE CASCADE', ]); $path = __DIR__.'/../../migrations'; if (! is_dir($path)) { mkdir($path, 0777, true); } $generator->definition()->formatter()->write($path); $this->assertFileExists($path.'/0000_00_00_000000_create_table_table.php'); } public function test_cleans_up_regular_morphs() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` int(9) unsigned NOT NULL', '`user_type` varchar(255) NOT NULL', '`note` varchar(255) NOT NULL', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->morphs(\'user\');', $schema); } public function test_doesnt_clean_up_morph_looking_columns() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` varchar(255) NOT NULL', '`user_type` varchar(255) NOT NULL', '`note` varchar(255) NOT NULL', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertStringNotContainsString('$table->morphs(\'user\');', $schema); } public function test_cleans_up_uuid_morphs() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` char(36) NOT NULL', '`user_type` varchar(255) NOT NULL', '`note` varchar(255) NOT NULL', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->uuidMorphs(\'user\');', $schema); } public function test_cleans_up_uuid_morphs_nullable() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT PRIMARY KEY', '`user_id` char(36) DEFAULT NULL', '`user_type` varchar(255) DEFAULT NULL', '`note` varchar(255) NOT NULL', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->nullableUuidMorphs(\'user\');', $schema); } public function test_doesnt_clean_non_auto_inc_id_to_laravel_method() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL', 'PRIMARY KEY `id`', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->unsignedInteger(\'id\')->primary();', $schema); } public function test_does_clean_auto_inc_int_to_laravel_method() { $generator = TableGenerator::init('table', [ '`id` int(9) unsigned NOT NULL AUTO_INCREMENT', 'PRIMARY KEY `id`', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->increments(\'id\');', $schema); } public function test_does_clean_auto_inc_big_int_to_laravel_method() { $generator = TableGenerator::init('table', [ '`id` bigint(12) unsigned NOT NULL AUTO_INCREMENT', 'PRIMARY KEY `id`', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->id();', $schema); } public function test_doesnt_clean_timestamps_with_use_current() { $generator = TableGenerator::init('table', [ 'id int auto_increment primary key', 'created_at timestamp not null default CURRENT_TIMESTAMP', 'updated_at timestamp null on update CURRENT_TIMESTAMP', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->timestamp(\'created_at\')->useCurrent()', $schema); $this->assertSchemaHas('$table->timestamp(\'updated_at\')->nullable()->useCurrentOnUpdate()', $schema); } public function test_doesnt_clean_timestamps_with_use_current_on_update() { $generator = TableGenerator::init('table', [ 'id int auto_increment primary key', 'created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP', 'updated_at timestamp null on update CURRENT_TIMESTAMP', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->timestamp(\'created_at\')->useCurrent()->useCurrentOnUpdate()', $schema); $this->assertSchemaHas('$table->timestamp(\'updated_at\')->nullable()->useCurrentOnUpdate()', $schema); } public function test_doesnt_clean_timestamps_with_use_defined_datatype_on_timestamp_configuration() { config()->set('laravel-migration-generator.definitions.use_defined_datatype_on_timestamp', true); $generator = TableGenerator::init('table', [ 'id int auto_increment primary key', 'created_at datetime NOT NULL', 'updated_at datetime NOT NULL', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->dateTime(\'created_at\')', $schema); $this->assertSchemaHas('$table->dateTime(\'updated_at\')', $schema); } public function test_removes_index_from_column_if_fk() { $generator = TableGenerator::init('test', [ '`import_id` bigint(20) unsigned DEFAULT NULL', '`import_service_id` bigint(20) unsigned DEFAULT NULL', 'KEY `fk_import_id` (`import_id`)', 'KEY `fk_import_service_id` (`import_service_id`)', 'CONSTRAINT `fk_import_id` FOREIGN KEY (`import_id`) REFERENCES `imports` (`id`)', 'CONSTRAINT `fk_import_service_id` FOREIGN KEY (`import_service_id`) REFERENCES `import_services` (`id`)', ]); $schema = $generator->definition()->formatter()->getSchema(); $this->assertSchemaHas('$table->unsignedBigInteger(\'import_id\')->nullable();', $schema); $this->assertSchemaHas('$table->unsignedBigInteger(\'import_service_id\')->nullable();', $schema); $this->assertSchemaHas('$table->foreign(\'import_id\', \'fk_import_id\')->references(\'id\')->on(\'imports\');', $schema); $this->assertSchemaHas('$table->foreign(\'import_service_id\', \'fk_import_service_id\')->references(\'id\')->on(\'import_services\');', $schema); } } ================================================ FILE: tests/Unit/Generators/MySQLViewGeneratorTest.php ================================================ cleanUpMigrations($path); } private function cleanUpMigrations($path) { if (is_dir($path)) { foreach (glob($path.'/*.php') as $file) { unlink($file); } rmdir($path); } } public function test_generates() { $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`'); $this->assertStringStartsWith('CREATE VIEW `view_client_config` AS', $generator->definition()->getSchema()); } public function test_writes() { $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`'); $path = __DIR__.'/../../migrations'; if (! is_dir($path)) { mkdir($path, 0777, true); } $written = $generator->definition()->formatter()->write($path); $this->assertFileExists($written); } } ================================================ FILE: tests/Unit/Tokenizers/MySQL/ColumnTokenizerTest.php ================================================ definition(); $this->assertEquals('email', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertEquals('$table->string(\'email\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_column() { $columnTokenizer = ColumnTokenizer::parse('`favorite_color` varchar(200) COLLATE utf8mb4_unicode_ci'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertEquals(200, $columnDefinition->getMethodParameters()[0]); $this->assertNull($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertEquals('$table->string(\'favorite_color\', 200)', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT \'orange\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertEquals('orange', $columnDefinition->getDefaultValue()); $this->assertEquals('$table->string(\'favorite_color\')->default(\'orange\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_default_value_null_column() { $columnTokenizer = ColumnTokenizer::parse('`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('$table->string(\'favorite_color\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_default_value_null_column_with_comment_with_setting() { $columnTokenizer = ColumnTokenizer::parse('`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT \'favorite color\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('favorite color', $columnDefinition->getComment()); $this->assertEquals('$table->string(\'favorite_color\')->nullable()->comment("favorite color")', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_default_value_null_column_with_comment_without_setting() { config()->set('laravel-migration-generator.definitions.with_comments', false); $columnTokenizer = ColumnTokenizer::parse('`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT \'favorite color\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('favorite color', $columnDefinition->getComment()); $this->assertEquals('$table->string(\'favorite_color\')->nullable()', $columnDefinition->render()); config()->set('laravel-migration-generator.definitions.with_comments', true); } public function test_it_tokenizes_a_null_varchar_default_value_null_column_with_comment_apostrophe() { $columnTokenizer = ColumnTokenizer::parse("`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'favorite color is ''green''"); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('favorite color is \'green\'', $columnDefinition->getComment()); $this->assertEquals('$table->string(\'favorite_color\')->nullable()->comment("favorite color is \'green\'")', $columnDefinition->render()); } public function test_it_tokenizes_a_null_varchar_default_value_null_column_with_comment_quotation() { $columnTokenizer = ColumnTokenizer::parse("`favorite_color` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'favorite color is \"green\""); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('favorite_color', $columnDefinition->getColumnName()); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('favorite color is "green"', $columnDefinition->getComment()); $this->assertEquals('$table->string(\'favorite_color\')->nullable()->comment("favorite color is \"green\"")', $columnDefinition->render()); } public function test_it_tokenizes_varchar_with_default_and_comment() { $columnTokenizer = ColumnTokenizer::parse("`testing` varchar(255) DEFAULT 'this is ''it''' COMMENT 'this is the \"comment\"'"); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('this is \'it\'', $columnDefinition->getDefaultValue()); $this->assertEquals('this is the "comment"', $columnDefinition->getComment()); } public function test_it_tokenizes_varchar_with_default_empty_string_and_comment() { $columnTokenizer = ColumnTokenizer::parse("`testing` varchar(255) DEFAULT '' COMMENT 'this is the \"comment\"'"); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('', $columnDefinition->getDefaultValue()); $this->assertEquals('this is the "comment"', $columnDefinition->getComment()); } public function test_it_tokenizes_varchar_with_default_empty_string_and_comment_with_apostrophe() { $columnTokenizer = ColumnTokenizer::parse("`testing` varchar(255) DEFAULT '' COMMENT 'this is the \"comment\" ''inside single quote''"); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('', $columnDefinition->getDefaultValue()); $this->assertEquals('this is the "comment" \'inside single quote\'', $columnDefinition->getComment()); } public function test_it_tokenizes_varchar_with_boolean_literal_default() { $columnTokenizer = ColumnTokenizer::parse("`testing` bit(2) DEFAULT b'10'"); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('bit', $columnDefinition->getMethodName()); $this->assertEquals('b\'10\'', $columnDefinition->getDefaultValue()); $this->assertEquals("\$table->bit('testing', 2)->default(b'10')", $columnDefinition->render()); } public function test_it_tokenizes_char_column_with_character_and_collation() { $columnTokenizer = ColumnTokenizer::parse('`country` char(2) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT \'US\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('country', $columnDefinition->getColumnName()); $this->assertEquals('char', $columnTokenizer->getColumnDataType()); $this->assertEquals('char', $columnDefinition->getMethodName()); $this->assertCount(1, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->isNullable()); $this->assertEquals('utf8mb4_unicode_ci', $columnDefinition->getCollation()); $this->assertEquals('utf8mb4', $columnDefinition->getCharacterSet()); $this->assertEquals('$table->char(\'country\', 2)->default(\'US\')', $columnDefinition->render()); } //endregion //region TEXT & Variants public function test_it_tokenizes_a_not_null_tinytext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` tinytext NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('tinytext', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->string(\'notes\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_tinytext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` tinytext'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('tinytext', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->string(\'notes\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_text_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` text NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('text', $columnTokenizer->getColumnDataType()); $this->assertEquals('text', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->text(\'notes\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_text_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` text'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('text', $columnTokenizer->getColumnDataType()); $this->assertEquals('text', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->text(\'notes\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_mediumtext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` mediumtext NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('mediumtext', $columnTokenizer->getColumnDataType()); $this->assertEquals('mediumText', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->mediumText(\'notes\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_mediumtext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` mediumtext'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('mediumtext', $columnTokenizer->getColumnDataType()); $this->assertEquals('mediumText', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->mediumText(\'notes\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_longtext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` longtext NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('longtext', $columnTokenizer->getColumnDataType()); $this->assertEquals('longText', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->longText(\'notes\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_longtext_column() { $columnTokenizer = ColumnTokenizer::parse('`notes` longtext'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('notes', $columnDefinition->getColumnName()); $this->assertEquals('longtext', $columnTokenizer->getColumnDataType()); $this->assertEquals('longText', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->longText(\'notes\')->nullable()', $columnDefinition->render()); } //endregion //region INT & Variants public function test_it_tokenizes_a_not_null_smallint_without_param_column() { $columnTokenizer = ColumnTokenizer::parse('`cats` smallint NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('cats', $columnDefinition->getColumnName()); $this->assertEquals('smallint', $columnTokenizer->getColumnDataType()); $this->assertEquals('smallInteger', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->smallInteger(\'cats\')', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_smallint_with_param_column() { $columnTokenizer = ColumnTokenizer::parse('`cats` smallint(6) NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('cats', $columnDefinition->getColumnName()); $this->assertEquals('smallint', $columnTokenizer->getColumnDataType()); $this->assertEquals('smallInteger', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->smallInteger(\'cats\')', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_unsigned_smallint_column() { $columnTokenizer = ColumnTokenizer::parse('`cats` smallint(6) unsigned NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('cats', $columnDefinition->getColumnName()); $this->assertEquals('smallint', $columnTokenizer->getColumnDataType()); $this->assertEquals('smallInteger', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->unsignedSmallInteger(\'cats\')', $columnDefinition->render()); } public function test_it_tokenizes_a_nullable_big_int_column() { $columnTokenizer = ColumnTokenizer::parse('`template_id` bigint(20) unsigned DEFAULT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('template_id', $columnDefinition->getColumnName()); $this->assertEquals('bigint', $columnTokenizer->getColumnDataType()); $this->assertEquals('bigInteger', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertTrue($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertEquals('$table->unsignedBigInteger(\'template_id\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_primary_auto_inc_int_column() { $columnTokenizer = ColumnTokenizer::parse('`id` int(9) unsigned NOT NULL AUTO_INCREMENT'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('id', $columnDefinition->getColumnName()); $this->assertEquals('int', $columnTokenizer->getColumnDataType()); $this->assertEquals('integer', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isPrimary()); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->unsignedInteger(\'id\')', $columnDefinition->render()); } public function test_definition_config() { config()->set('laravel-migration-generator.definitions.prefer_unsigned_prefix', false); $columnTokenizer = ColumnTokenizer::parse('`column` int(9) unsigned NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('$table->integer(\'column\')->unsigned()', $columnDefinition->render()); config()->set('laravel-migration-generator.definitions.prefer_unsigned_prefix', true); } //endregion //region FLOAT public function test_it_tokenizes_float_without_params_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\')', $columnDefinition->render()); } public function test_it_tokenizes_float_with_params_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float(4,2) NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(4, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\', 4, 2)', $columnDefinition->render()); } public function test_it_tokenizes_float_null_without_params_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\')', $columnDefinition->render()); } public function test_it_tokenizes_float_null_with_params_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float(4,2)'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(4, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\', 4, 2)', $columnDefinition->render()); } public function test_it_tokenizes_float_without_params_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals(1.0, $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\')->default(1.00)', $columnDefinition->render()); } public function test_it_tokenizes_float_with_params_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float(4,2) NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(4, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertEquals(1.00, $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\', 4, 2)->default(1.00)', $columnDefinition->render()); } public function test_it_tokenizes_float_null_without_params_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float DEFAULT 1.0'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals(1.0, $columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\')->default(1.0)', $columnDefinition->render()); } public function test_it_tokenizes_float_null_with_params_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`parameter` float(4,2) DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('parameter', $columnDefinition->getColumnName()); $this->assertEquals('float', $columnTokenizer->getColumnDataType()); $this->assertEquals('float', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(4, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertEquals(1.00, $columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->float(\'parameter\', 4, 2)->default(1.00)', $columnDefinition->render()); } //endregion //region DECIMAL public function test_it_tokenizes_a_not_null_decimal_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` decimal(9,2) NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->decimal(\'amount\', 9, 2)', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_unsigned_decimal_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` decimal(9,2) unsigned NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->unsignedDecimal(\'amount\', 9, 2)', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_decimal_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` decimal(9,2) NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals(1.0, $columnDefinition->getDefaultValue()); $this->assertEquals('$table->decimal(\'amount\', 9, 2)->default(1.00)', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_unsigned_decimal_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` decimal(9,2) unsigned NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertEquals(1.0, $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->unsignedDecimal(\'amount\', 9, 2)->default(1.00)', $columnDefinition->render()); } //endregion //region DOUBLE public function test_it_tokenizes_a_not_null_double_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` double(9,2) NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('double', $columnTokenizer->getColumnDataType()); $this->assertEquals('double', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->double(\'amount\', 9, 2)', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_unsigned_double_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` double(9,2) unsigned NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('double', $columnTokenizer->getColumnDataType()); $this->assertEquals('double', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->double(\'amount\', 9, 2)->unsigned()', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_double_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` double(9,2) NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('double', $columnTokenizer->getColumnDataType()); $this->assertEquals('double', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals(1.00, $columnDefinition->getDefaultValue()); $this->assertEquals('$table->double(\'amount\', 9, 2)->default(1.00)', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_unsigned_double_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`amount` double(9,2) unsigned NOT NULL DEFAULT 1.00'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('amount', $columnDefinition->getColumnName()); $this->assertEquals('double', $columnTokenizer->getColumnDataType()); $this->assertEquals('double', $columnDefinition->getMethodName()); $this->assertCount(2, $columnDefinition->getMethodParameters()); $this->assertEquals(9, $columnDefinition->getMethodParameters()[0]); $this->assertEquals(2, $columnDefinition->getMethodParameters()[1]); $this->assertEquals(1.00, $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertTrue($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->double(\'amount\', 9, 2)->unsigned()->default(1.00)', $columnDefinition->render()); } //endregion //region DATETIME public function test_it_tokenizes_a_not_null_datetime_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_datetime_default_now_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')->useCurrent()', $columnDefinition->render()); } public function test_it_tokenizes_a_null_datetime_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_default_value_datetime_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime DEFAULT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertTrue($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_a_null_default_value_now_datetime_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime DEFAULT CURRENT_TIMESTAMP'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')->useCurrent()', $columnDefinition->render()); } public function test_it_tokenizes_a_null_default_value_now_and_on_update_datetime_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('datetime', $columnTokenizer->getColumnDataType()); $this->assertEquals('dateTime', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->dateTime(\'sent_at\')->useCurrent()->useCurrentOnUpdate()', $columnDefinition->render()); } //endregion //region TIMESTAMP public function test_it_tokenizes_a_not_null_timestamp_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` timestamp NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('timestamp', $columnTokenizer->getColumnDataType()); $this->assertEquals('timestamp', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->timestamp(\'sent_at\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_timestamp_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` timestamp'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('timestamp', $columnTokenizer->getColumnDataType()); $this->assertEquals('timestamp', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertNull($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->timestamp(\'sent_at\')', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_use_current_timestamp_timestamp_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('timestamp', $columnTokenizer->getColumnDataType()); $this->assertEquals('timestamp', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->timestamp(\'sent_at\')->useCurrent()', $columnDefinition->render()); } public function test_it_tokenizes_a_not_null_default_value_timestamp_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` timestamp NOT NULL DEFAULT \'2000-01-01 00:00:01\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('timestamp', $columnTokenizer->getColumnDataType()); $this->assertEquals('timestamp', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('2000-01-01 00:00:01', $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->timestamp(\'sent_at\')->default(\'2000-01-01 00:00:01\')', $columnDefinition->render()); } public function test_it_tokenizes_a_null_default_value_timestamp_column() { $columnTokenizer = ColumnTokenizer::parse('`sent_at` timestamp NULL DEFAULT \'2000-01-01 00:00:01\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('sent_at', $columnDefinition->getColumnName()); $this->assertEquals('timestamp', $columnTokenizer->getColumnDataType()); $this->assertEquals('timestamp', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('2000-01-01 00:00:01', $columnDefinition->getDefaultValue()); $this->assertTrue($columnDefinition->isNullable()); $this->assertFalse($columnDefinition->isUnsigned()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->timestamp(\'sent_at\')->nullable()->default(\'2000-01-01 00:00:01\')', $columnDefinition->render()); } //endregion //region ENUM public function test_it_tokenizes_enum_column() { $columnTokenizer = ColumnTokenizer::parse('`status_flag` enum(\'1\',\'2\',\'3\',\'4\')'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('status_flag', $columnDefinition->getColumnName()); $this->assertEquals('enum', $columnTokenizer->getColumnDataType()); $this->assertEquals('enum', $columnDefinition->getMethodName()); $this->assertCount(4, $columnDefinition->getMethodParameters()[0]); $this->assertEqualsCanonicalizing([1, 2, 3, 4], $columnDefinition->getMethodParameters()[0]); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->enum(\'status_flag\', [\'1\', \'2\', \'3\', \'4\'])', $columnDefinition->render()); } public function test_it_tokenizes_enum_column_with_upper_case_values() { $columnTokenizer = ColumnTokenizer::parse('`film_rating` enum(\'G\',\'PG\',\'PG-13\',\'R\',\'NC-17\')'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('film_rating', $columnDefinition->getColumnName()); $this->assertEquals('enum', $columnTokenizer->getColumnDataType()); $this->assertEquals('enum', $columnDefinition->getMethodName()); $this->assertCount(5, $columnDefinition->getMethodParameters()[0]); $this->assertEqualsCanonicalizing(['G', 'PG', 'PG-13', 'R', 'NC-17'], $columnDefinition->getMethodParameters()[0]); $this->assertNull($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->enum(\'film_rating\', [\'G\', \'PG\', \'PG-13\', \'R\', \'NC-17\'])', $columnDefinition->render()); } public function test_it_tokenizes_not_null_enum_column() { $columnTokenizer = ColumnTokenizer::parse('`status_flag` enum(\'1\',\'2\',\'3\',\'4\') NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('status_flag', $columnDefinition->getColumnName()); $this->assertEquals('enum', $columnTokenizer->getColumnDataType()); $this->assertEquals('enum', $columnDefinition->getMethodName()); $this->assertCount(4, $columnDefinition->getMethodParameters()[0]); $this->assertEqualsCanonicalizing([1, 2, 3, 4], $columnDefinition->getMethodParameters()[0]); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->enum(\'status_flag\', [\'1\', \'2\', \'3\', \'4\'])', $columnDefinition->render()); } public function test_it_tokenizes_enum_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`status_flag` enum(\'1\',\'2\',\'3\',\'4\') DEFAULT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('status_flag', $columnDefinition->getColumnName()); $this->assertEquals('enum', $columnTokenizer->getColumnDataType()); $this->assertEquals('enum', $columnDefinition->getMethodName()); $this->assertCount(4, $columnDefinition->getMethodParameters()[0]); $this->assertEqualsCanonicalizing([1, 2, 3, 4], $columnDefinition->getMethodParameters()[0]); $this->assertNull($columnDefinition->getDefaultValue()); $this->assertTrue($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->enum(\'status_flag\', [\'1\', \'2\', \'3\', \'4\'])->nullable()', $columnDefinition->render()); } public function test_it_tokenizes_not_null_enum_with_default_value_column() { $columnTokenizer = ColumnTokenizer::parse('`status_flag` enum(\'1\',\'2\',\'3\',\'4\') NOT NULL DEFAULT \'1\''); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('status_flag', $columnDefinition->getColumnName()); $this->assertEquals('enum', $columnTokenizer->getColumnDataType()); $this->assertEquals('enum', $columnDefinition->getMethodName()); $this->assertCount(4, $columnDefinition->getMethodParameters()[0]); $this->assertEqualsCanonicalizing([1, 2, 3, 4], $columnDefinition->getMethodParameters()[0]); $this->assertEquals('1', $columnDefinition->getDefaultValue()); $this->assertFalse($columnDefinition->isNullable()); $this->assertNull($columnDefinition->getCollation()); $this->assertEquals('$table->enum(\'status_flag\', [\'1\', \'2\', \'3\', \'4\'])->default(\'1\')', $columnDefinition->render()); } public function test_it_tokenizes_enum_with_spaces() { $columnTokenizer = ColumnTokenizer::parse('`calculate` enum(\'one\',\'and\',\'highest or\',\'lowest or\',\'sum\',\'highest position or\',\'lowest position or\') COLLATE utf8mb4_general_ci NOT NULL COMMENT \'set the way we calculate a feature value. with high or low or the sort is by position\''); $definition = $columnTokenizer->definition(); $this->assertEquals('$table->enum(\'calculate\', [\'one\', \'and\', \'highest or\', \'lowest or\', \'sum\', \'highest position or\', \'lowest position or\'])->comment("set the way we calculate a feature value. with high or low or the sort is by position")', $definition->render()); } public function test_it_tokenizes_enum_with_special_characters() { $columnTokenizer = ColumnTokenizer::parse('`calculate` enum(\'one\',\'and\',\'highest-or\',\'lowest^or\',\'sum%\',\'highest $ position or\',\'lowest+_<>?/ position or\',\'"quoted"\') COLLATE utf8mb4_general_ci NOT NULL COMMENT \'set the way we calculate a feature value. with high or low or the sort is by position\''); $definition = $columnTokenizer->definition(); $this->assertEquals('$table->enum(\'calculate\', [\'one\', \'and\', \'highest-or\', \'lowest^or\', \'sum%\', \'highest $ position or\', \'lowest+_<>?/ position or\', \'"quoted"\'])->comment("set the way we calculate a feature value. with high or low or the sort is by position")', $definition->render()); } public function test_it_tokenizes_enum_with_empty_string() { $columnTokenizer = ColumnTokenizer::parse('`text` enum(\'\',\'not-empty-string\',\'string with spaces\') DEFAULT \'\''); $definition = $columnTokenizer->definition(); $this->assertEquals('$table->enum(\'text\', [\'\', \'not-empty-string\', \'string with spaces\'])->default(\'\')', $definition->render()); } //endregion //region POINT, MULTIPOINT public function test_it_tokenizes_point_column() { $columnTokenizer = ColumnTokenizer::parse('`point` point NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('point', $columnTokenizer->getColumnDataType()); $this->assertEquals('point', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('$table->point(\'point\')', $columnDefinition->render()); } public function test_it_tokenizes_multipoint_column() { $columnTokenizer = ColumnTokenizer::parse('`point` multipoint NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('multipoint', $columnTokenizer->getColumnDataType()); $this->assertEquals('multiPoint', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('$table->multiPoint(\'point\')', $columnDefinition->render()); } //endregion //region POLYGON, MULTIPOLYGON public function test_it_tokenizes_polygon_column() { $columnTokenizer = ColumnTokenizer::parse('`polygon` polygon NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('polygon', $columnTokenizer->getColumnDataType()); $this->assertEquals('polygon', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('$table->polygon(\'polygon\')', $columnDefinition->render()); } public function test_it_tokenizes_multipolygon_column() { $columnTokenizer = ColumnTokenizer::parse('`polygon` multipolygon NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('multipolygon', $columnTokenizer->getColumnDataType()); $this->assertEquals('multiPolygon', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertEquals('$table->multiPolygon(\'polygon\')', $columnDefinition->render()); } //endregion, //region GEOMETRY public function test_it_tokenizes_geometry_column() { $columnTokenizer = ColumnTokenizer::parse('`geometry` geometry NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('geometry', $columnTokenizer->getColumnDataType()); $this->assertEquals('geometry', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); } public function test_it_tokenizes_geometry_collection_column() { $columnTokenizer = ColumnTokenizer::parse('`geometry_collection` geometrycollection NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('geometrycollection', $columnTokenizer->getColumnDataType()); $this->assertEquals('geometryCollection', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); } //endregion //region SET public function test_it_tokenizes_set_column() { $columnTokenizer = ColumnTokenizer::parse('`set_field` set(\'1\',\'2\',\'3\') COLLATE utf8mb4_unicode_ci DEFAULT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('set', $columnTokenizer->getColumnDataType()); $this->assertEquals('set', $columnDefinition->getMethodName()); $this->assertCount(1, $columnDefinition->getMethodParameters()); $this->assertNotNull($columnDefinition->getCollation()); $this->assertTrue($columnDefinition->isNullable()); $this->assertEquals('$table->set(\'set_field\', [\'1\', \'2\', \'3\'])->nullable()', $columnDefinition->render()); } //endregion //region UUID public function test_it_tokenizes_uuid_column() { $columnTokenizer = ColumnTokenizer::parse('`uuid_col` char(36) COLLATE utf8mb4_unicode_ci NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('char', $columnTokenizer->getColumnDataType()); $this->assertEquals('char', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNotNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->uuid(\'uuid_col\')', $columnDefinition->render()); } //endregion //region DATE, YEAR, TIME public function test_it_tokenizes_date_column() { $columnTokenizer = ColumnTokenizer::parse('`birth_date` date NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('date', $columnTokenizer->getColumnDataType()); $this->assertEquals('date', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->date(\'birth_date\')', $columnDefinition->render()); } public function test_it_tokenizes_year_column() { $columnTokenizer = ColumnTokenizer::parse('`birth_year` year NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('year', $columnTokenizer->getColumnDataType()); $this->assertEquals('year', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->year(\'birth_year\')', $columnDefinition->render()); } public function test_it_tokenizes_time_column() { $columnTokenizer = ColumnTokenizer::parse('`birth_time` time NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('time', $columnTokenizer->getColumnDataType()); $this->assertEquals('time', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->time(\'birth_time\')', $columnDefinition->render()); } //endregion //region LINESTRING, MULTILINESTRING public function test_it_tokenizes_linestring_column() { $columnTokenizer = ColumnTokenizer::parse('`str` linestring NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('linestring', $columnTokenizer->getColumnDataType()); $this->assertEquals('lineString', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->lineString(\'str\')', $columnDefinition->render()); } public function test_it_tokenizes_multilinestring_column() { $columnTokenizer = ColumnTokenizer::parse('`str` multilinestring NOT NULL'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('multilinestring', $columnTokenizer->getColumnDataType()); $this->assertEquals('multiLineString', $columnDefinition->getMethodName()); $this->assertCount(0, $columnDefinition->getMethodParameters()); $this->assertNull($columnDefinition->getCollation()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('$table->multiLineString(\'str\')', $columnDefinition->render()); } //endregion public function test_it_tokenizes_generated_as_column() { $columnTokenizer = ColumnTokenizer::parse('`total` decimal(24,6) GENERATED ALWAYS AS ((`quantity` * `unit_price`)) STORED'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('(`quantity` * `unit_price`)', $columnDefinition->getStoredAs()); $this->assertEquals('$table->decimal(\'total\', 24, 6)->storedAs("(`quantity` * `unit_price`)")', $columnDefinition->render()); } public function test_it_tokenizes_generated_as_column_example() { $columnTokenizer = ColumnTokenizer::parse('`full_name` varchar(150) COLLATE utf8mb4_unicode_ci GENERATED ALWAYS AS (concat(`first_name`,\' \',`last_name`)) STORED'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('varchar', $columnTokenizer->getColumnDataType()); $this->assertEquals('string', $columnDefinition->getMethodName()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('(concat(`first_name`,\' \',`last_name`))', $columnDefinition->getStoredAs()); $this->assertEquals('$table->string(\'full_name\', 150)->storedAs("(concat(`first_name`,\' \',`last_name`))")', $columnDefinition->render()); } public function test_it_tokenizes_virtual_as_column() { $columnTokenizer = ColumnTokenizer::parse('`total` decimal(24,6) AS ((`quantity` * `unit_price`))'); $columnDefinition = $columnTokenizer->definition(); $this->assertEquals('decimal', $columnTokenizer->getColumnDataType()); $this->assertEquals('decimal', $columnDefinition->getMethodName()); $this->assertFalse($columnDefinition->isNullable()); $this->assertEquals('(`quantity` * `unit_price`)', $columnDefinition->getVirtualAs()); $this->assertEquals('$table->decimal(\'total\', 24, 6)->virtualAs("(`quantity` * `unit_price`)")', $columnDefinition->render()); } } ================================================ FILE: tests/Unit/Tokenizers/MySQL/IndexTokenizerTest.php ================================================ definition(); $this->assertEquals('index', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->index([\'email\'], \'password_resets_email_index\')', $indexDefinition->render()); } public function test_it_doesnt_use_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); $indexTokenizer = IndexTokenizer::parse('KEY `password_resets_email_index` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('index', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->index([\'email\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); } //endregion //region Primary Key public function test_it_tokenizes_simple_primary_key() { $indexTokenizer = IndexTokenizer::parse('PRIMARY KEY (`id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('primary', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->primary([\'id\'])', $indexDefinition->render()); } public function test_it_tokenizes_two_column_primary_key() { $indexTokenizer = IndexTokenizer::parse('PRIMARY KEY (`email`,`token`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('primary', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEqualsCanonicalizing(['email', 'token'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->primary([\'email\', \'token\'])', $indexDefinition->render()); } //endregion //region Unique Key public function test_it_tokenizes_simple_unique_key() { $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_unique` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('unique', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->unique([\'email\'], \'users_email_unique\')', $indexDefinition->render()); } public function test_it_doesnt_use_unique_key_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', false); $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_unique` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('unique', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->unique([\'email\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', true); } public function test_it_tokenizes_two_column_unique_key() { $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_location_id_unique` (`email`,`location_id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('unique', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEqualsCanonicalizing(['email', 'location_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->unique([\'email\', \'location_id\'], \'users_email_location_id_unique\')', $indexDefinition->render()); } public function test_it_tokenizes_two_column_unique_key_and_doesnt_use_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', false); $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `users_email_location_id_unique` (`email`,`location_id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('unique', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEqualsCanonicalizing(['email', 'location_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->unique([\'email\', \'location_id\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_unique_key_index_names', true); } //endregion //region Foreign Constraints public function test_it_tokenizes_foreign_key() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_doesnt_use_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_foreign_key_index_names', false); $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\')->references(\'id\')->on(\'users\')', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_foreign_key_index_names', true); } public function test_it_tokenizes_foreign_key_with_update() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'cascade\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_with_delete() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onDelete(\'cascade\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_with_update_and_delete() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'cascade\')->onDelete(\'cascade\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_with_multiple_columns() { $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'); $definition = $indexTokenizer->definition(); $this->assertEquals('foreign', $definition->getIndexType()); $this->assertTrue($definition->isMultiColumnIndex()); $this->assertCount(2, $definition->getIndexColumns()); $this->assertEquals('table1', $definition->getForeignReferencedTable()); $this->assertSame(['table1-field1', 'table1-field2'], $definition->getForeignReferencedColumns()); $this->assertSame(['table2-foreign1', 'table2-foreign2'], $definition->getIndexColumns()); $this->assertEquals('$table->foreign([\'table2-foreign1\', \'table2-foreign2\'], \'table2_ibfk_1\')->references([\'table1-field1\', \'table1-field2\'])->on(\'table1\')->onDelete(\'cascade\')->onUpdate(\'cascade\')', $definition->render()); } public function test_it_tokenizes_foreign_key_with_update_restrict() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE NO ACTION'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'restrict\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_with_update_set_null() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE SET NULL'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'set NULL\')', $indexDefinition->render()); } public function test_it_tokenizes_foreign_key_with_update_set_default() { $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_bank_accounts_user_id` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE SET DEFAULT'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertCount(1, $indexDefinition->getIndexColumns()); $this->assertEquals('users', $indexDefinition->getForeignReferencedTable()); $this->assertEquals(['id'], $indexDefinition->getForeignReferencedColumns()); $this->assertEquals(['user_id'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->foreign(\'user_id\', \'fk_bank_accounts_user_id\')->references(\'id\')->on(\'users\')->onUpdate(\'set DEFAULT\')', $indexDefinition->render()); } //endregion //region Fulltext Index public function test_it_tokenizes_simple_fulltext_index() { $indexTokenizer = IndexTokenizer::parse('FULLTEXT KEY `posts_content_fulltext` (`content`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('fulltext', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals(['content'], $indexDefinition->getIndexColumns()); $this->assertEquals('posts_content_fulltext', $indexDefinition->getIndexName()); $this->assertEquals('$table->fullText([\'content\'], \'posts_content_fulltext\')', $indexDefinition->render()); } public function test_it_tokenizes_fulltext_index_without_using_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); $indexTokenizer = IndexTokenizer::parse('FULLTEXT KEY `posts_content_fulltext` (`content`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('fulltext', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->fullText([\'content\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); } public function test_it_tokenizes_multi_column_fulltext_index() { $indexTokenizer = IndexTokenizer::parse('FULLTEXT KEY `posts_title_content_fulltext` (`title`,`content`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('fulltext', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEqualsCanonicalizing(['title', 'content'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->fullText([\'title\', \'content\'], \'posts_title_content_fulltext\')', $indexDefinition->render()); } public function test_it_tokenizes_multi_column_fulltext_index_without_using_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); $indexTokenizer = IndexTokenizer::parse('FULLTEXT KEY `posts_title_content_fulltext` (`title`,`content`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('fulltext', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEquals('$table->fullText([\'title\', \'content\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); } //endregion //region Spatial Index public function test_it_tokenizes_simple_spatial_index() { $indexTokenizer = IndexTokenizer::parse('SPATIAL KEY `locations_coordinates_spatial` (`coordinates`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('spatial', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals(['coordinates'], $indexDefinition->getIndexColumns()); $this->assertEquals('locations_coordinates_spatial', $indexDefinition->getIndexName()); $this->assertEquals('$table->spatialIndex([\'coordinates\'], \'locations_coordinates_spatial\')', $indexDefinition->render()); } public function test_it_tokenizes_spatial_index_without_using_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); $indexTokenizer = IndexTokenizer::parse('SPATIAL KEY `locations_coordinates_spatial` (`coordinates`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('spatial', $indexDefinition->getIndexType()); $this->assertFalse($indexDefinition->isMultiColumnIndex()); $this->assertEquals('$table->spatialIndex([\'coordinates\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); } public function test_it_tokenizes_multi_column_spatial_index() { $indexTokenizer = IndexTokenizer::parse('SPATIAL KEY `locations_lat_lng_spatial` (`latitude`,`longitude`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('spatial', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEqualsCanonicalizing(['latitude', 'longitude'], $indexDefinition->getIndexColumns()); $this->assertEquals('$table->spatialIndex([\'latitude\', \'longitude\'], \'locations_lat_lng_spatial\')', $indexDefinition->render()); } public function test_it_tokenizes_multi_column_spatial_index_without_using_index_name() { config()->set('laravel-migration-generator.definitions.use_defined_index_names', false); $indexTokenizer = IndexTokenizer::parse('SPATIAL KEY `locations_lat_lng_spatial` (`latitude`,`longitude`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('spatial', $indexDefinition->getIndexType()); $this->assertTrue($indexDefinition->isMultiColumnIndex()); $this->assertCount(2, $indexDefinition->getIndexColumns()); $this->assertEquals('$table->spatialIndex([\'latitude\', \'longitude\'])', $indexDefinition->render()); config()->set('laravel-migration-generator.definitions.use_defined_index_names', true); } //endregion //region Security - Index Name Escaping public function test_it_escapes_single_quotes_in_index_names() { $indexTokenizer = IndexTokenizer::parse('KEY `idx_test\'s_index` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('index', $indexDefinition->getIndexType()); // The rendered output should escape the single quote to prevent PHP injection $this->assertEquals('$table->index([\'email\'], \'idx_test\\\'s_index\')', $indexDefinition->render()); } public function test_it_escapes_single_quotes_in_fulltext_index_names() { $indexTokenizer = IndexTokenizer::parse('FULLTEXT KEY `idx_test\'s_fulltext` (`content`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('fulltext', $indexDefinition->getIndexType()); // The rendered output should escape the single quote to prevent PHP injection $this->assertEquals('$table->fullText([\'content\'], \'idx_test\\\'s_fulltext\')', $indexDefinition->render()); } public function test_it_escapes_single_quotes_in_spatial_index_names() { $indexTokenizer = IndexTokenizer::parse('SPATIAL KEY `idx_test\'s_spatial` (`coordinates`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('spatial', $indexDefinition->getIndexType()); // The rendered output should escape the single quote to prevent PHP injection $this->assertEquals('$table->spatialIndex([\'coordinates\'], \'idx_test\\\'s_spatial\')', $indexDefinition->render()); } public function test_it_escapes_single_quotes_in_column_names() { $indexTokenizer = IndexTokenizer::parse('KEY `test_index` (`col\'s_name`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('index', $indexDefinition->getIndexType()); // The rendered output should escape the single quote in column name $this->assertEquals('$table->index([\'col\\\'s_name\'], \'test_index\')', $indexDefinition->render()); } public function test_it_escapes_backslashes_in_index_names() { $indexTokenizer = IndexTokenizer::parse('KEY `idx\\test` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('index', $indexDefinition->getIndexType()); // Backslashes should be escaped $this->assertEquals('$table->index([\'email\'], \'idx\\\\test\')', $indexDefinition->render()); } public function test_it_escapes_backslashes_and_quotes_in_unique_index_names() { $indexTokenizer = IndexTokenizer::parse('UNIQUE KEY `idx\\\'test` (`email`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('unique', $indexDefinition->getIndexType()); // Both backslashes and quotes should be escaped $this->assertEquals('$table->unique([\'email\'], \'idx\\\\\\\'test\')', $indexDefinition->render()); } public function test_it_escapes_single_quotes_in_foreign_key_index_names() { config()->set('laravel-migration-generator.definitions.use_defined_foreign_key_index_names', true); $indexTokenizer = IndexTokenizer::parse('CONSTRAINT `fk_test\'s_key` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)'); $indexDefinition = $indexTokenizer->definition(); $this->assertEquals('foreign', $indexDefinition->getIndexType()); // The rendered output should escape the single quote $this->assertEquals('$table->foreign(\'user_id\', \'fk_test\\\'s_key\')->references(\'id\')->on(\'users\')', $indexDefinition->render()); } //endregion } ================================================ FILE: tests/Unit/ValueToStringTest.php ================================================ assertEquals('null', ValueToString::make(null)); } public function test_it_returns_integer_as_is() { $this->assertEquals(42, ValueToString::make(42)); } public function test_it_returns_float_as_is() { $this->assertEquals(3.14, ValueToString::make(3.14)); } public function test_it_quotes_string_value() { $this->assertEquals("'hello'", ValueToString::make('hello')); } public function test_it_uses_double_quotes_when_specified() { $this->assertEquals('"hello"', ValueToString::make('hello', false, false)); } public function test_it_returns_array_as_bracketed_list() { $this->assertEquals("['one', 'two', 'three']", ValueToString::make(['one', 'two', 'three'])); } public function test_it_singles_out_array_when_option_is_true() { $this->assertEquals("'single'", ValueToString::make(['single'], true)); } public function test_it_does_not_single_out_multi_element_array() { $this->assertEquals("['one', 'two']", ValueToString::make(['one', 'two'], true)); } //endregion //region Escape Functionality public function test_escape_escapes_single_quotes_by_default() { $this->assertEquals("test\\'s value", ValueToString::escape("test's value")); } public function test_escape_escapes_double_quotes_when_specified() { $this->assertEquals('test\\"s value', ValueToString::escape('test"s value', false)); } public function test_escape_escapes_backslashes() { $this->assertEquals('path\\\\to\\\\file', ValueToString::escape('path\\to\\file')); } public function test_escape_escapes_both_backslashes_and_single_quotes() { $this->assertEquals("it\\'s a \\\\path", ValueToString::escape("it's a \\path")); } public function test_escape_escapes_both_backslashes_and_double_quotes() { $this->assertEquals('it\\"s a \\\\path', ValueToString::escape('it"s a \\path', false)); } public function test_escape_handles_empty_string() { $this->assertEquals('', ValueToString::escape('')); } //endregion //region Security Tests - PHP Injection Prevention public function test_make_escapes_single_quotes_in_string() { $malicious = "test'); phpinfo();//"; $result = ValueToString::make($malicious); // The result should have escaped quotes $this->assertEquals("'test\\'); phpinfo();//'", $result); // Verify it doesn't break out of string context $this->assertStringNotContainsString("''", $result); } public function test_make_escapes_single_quotes_in_array() { $malicious = ["col'); phpinfo();//"]; $result = ValueToString::make($malicious); $this->assertEquals("['col\\'); phpinfo();//']", $result); } public function test_make_escapes_single_quotes_in_singled_out_array() { $malicious = ["col'); phpinfo();//"]; $result = ValueToString::make($malicious, true); $this->assertEquals("'col\\'); phpinfo();//'", $result); } public function test_make_escapes_backslashes_followed_by_quotes() { // Input: test\' (backslash followed by single quote) // Output: 'test\\\'' - outer quotes, then \\ for backslash, \' for quote $value = "test\\'"; $result = ValueToString::make($value); // 10 characters total: ' t e s t \\ \\ \' ' $this->assertEquals(10, strlen($result)); $this->assertEquals("'test\\\\\\''", $result); } public function test_make_escapes_complex_injection_attempt() { $malicious = "idx\\'; system('whoami');//"; $result = ValueToString::make($malicious); // Backslashes and quotes should be escaped $this->assertEquals("'idx\\\\\\'; system(\\'whoami\\');//'", $result); } public function test_make_escapes_multiple_quotes_in_array() { $malicious = ["col'1", "col'2"]; $result = ValueToString::make($malicious); $this->assertEquals("['col\\'1', 'col\\'2']", $result); } //endregion //region Cast Value Tests public function test_cast_float_creates_casted_value() { $result = ValueToString::castFloat('3.14'); $this->assertEquals('float$:3.14', $result); } public function test_cast_binary_creates_casted_value() { $result = ValueToString::castBinary('0101'); $this->assertEquals('binary$:0101', $result); } public function test_is_casted_value_detects_float() { $this->assertTrue(ValueToString::isCastedValue('float$:3.14')); } public function test_is_casted_value_detects_binary() { $this->assertTrue(ValueToString::isCastedValue('binary$:0101')); } public function test_is_casted_value_returns_false_for_regular_string() { $this->assertFalse(ValueToString::isCastedValue('regular string')); } public function test_make_handles_casted_float() { $casted = ValueToString::castFloat('3.14'); $result = ValueToString::make($casted); $this->assertEquals('3.14', $result); } public function test_make_handles_casted_binary() { $casted = ValueToString::castBinary('0101'); $result = ValueToString::make($casted); $this->assertEquals("b'0101'", $result); } //endregion }