├── .github ├── FUNDING.yml └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── .gitignore ├── .gitmodules ├── LICENSE ├── Readme.md ├── composer.json └── src ├── Attributes ├── Applicable.php ├── BigIncrements.php ├── BigInteger.php ├── Binary.php ├── CFloat.php ├── CString.php ├── Char.php ├── Column.php ├── ColumnAlias.php ├── Computed.php ├── DateTime.php ├── DateTimeTz.php ├── Decimal.php ├── Enum.php ├── Exception.php ├── ForeignKey.php ├── Geography.php ├── Geometry.php ├── Increments.php ├── Index.php ├── IndexType.php ├── Integer.php ├── MediumIncrements.php ├── MediumInteger.php ├── MigrationAttribute.php ├── Off.php ├── PivotColumn.php ├── PivotTable.php ├── Primary.php ├── Relationship.php ├── Set.php ├── SmallIncrements.php ├── SmallInteger.php ├── Table.php ├── Time.php ├── TimeTz.php ├── Timestamp.php ├── TimestampTz.php ├── TinyIncrements.php ├── TinyInteger.php ├── Unique.php ├── UnsignedBigInteger.php ├── UnsignedInteger.php ├── UnsignedMediumInteger.php ├── UnsignedSmallInteger.php ├── UnsignedTinyInteger.php └── composer.json ├── Blueprint ├── BlueprintDiff.php ├── Exporters │ ├── ColumnDiffExporter.php │ ├── ColumnExporter.php │ ├── Exporter.php │ ├── IndexExporter.php │ ├── SortsColumns.php │ ├── TableDiffExporter.php │ └── TableExporter.php ├── Manager.php ├── Migratable.php ├── Relationships │ ├── DirectRelationship.php │ ├── IndirectRelationship.php │ ├── MorphicDirectRelationship.php │ ├── MorphicIndirectRelationship.php │ ├── Polymorphic.php │ └── Relationship.php └── SimplifyingBlueprint.php ├── Console └── Commands │ └── GenerateMigrationCommand.php ├── Generator ├── MigrationGenerator.php ├── MigrationMode.php ├── RelationshipResolver.php └── TemplateManager.php ├── Providers └── ImplicitMigrationsServiceProvider.php ├── config └── database.php └── templates ├── migration-create.php.tpl └── migration-update.php.tpl /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: toramanlis 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.coverage/ 2 | /.devcontainer/ 3 | /.devcontainer.example/ 4 | /.github/ 5 | /.phpunit.cache/ 6 | /.vscode/ 7 | /.wiki/ 8 | /tests/ 9 | /vendor/ 10 | .phpcs.xml 11 | phpunit.xml 12 | phpunit.xml.bak 13 | coverage.xml 14 | composer.lock -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toramanlis/laravel-implicit-migrations/66653b4657ad98c23cec373ff81fdc36134ac85f/.gitmodules -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 toramanlis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | [![codecov](https://codecov.io/gh/toramanlis/laravel-implicit-migrations/graph/badge.svg?token=BH5VBNIWMI)](https://codecov.io/gh/toramanlis/laravel-implicit-migrations) 2 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/toramanlis/laravel-implicit-migrations.svg?style=flat-square)](https://packagist.org/packages/toramanlis/laravel-implicit-migrations) 3 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 4 | [![Total Downloads](https://img.shields.io/packagist/dt/toramanlis/laravel-implicit-migrations.svg?style=flat-square)](https://packagist.org/packages/toramanlis/laravel-implicit-migrations) 5 | 6 | ![Laravel Implicit Migrations](https://repository-images.githubusercontent.com/853000736/e44bfe61-b6ff-46cb-87f8-0c5b67e6c438) 7 | 8 | - [Overview](#overview) 9 | - [What It Is](#what-it-is) 10 | - [How It Works](#how-it-works) 11 | - [Implications](#implications) 12 | - [Annotations](#annotations) 13 | - [PHP Attributes](#php-attributes) 14 | - [Updates](#updates) 15 | - [Installation](#installation) 16 | - [Publishing The Attributes](#publishing-the-attributes) 17 | - [Opting Out From Attributes](#opting-out-from-attributes) 18 | - [Installing To Production](#installing-to-production) 19 | - [Configuration](#configuration) 20 | - [`database.model_paths`](#databasemodel_paths) 21 | - [`database.auto_infer_migrations`](#databaseauto_infer_migrations) 22 | - [`database.implications.`](#databaseimplicationsimplication_name_in_snake_case) 23 | - [Manual Migrations](#manual-migrations) 24 | - [Implication Reference](#implication-reference) 25 | - [`Table`](#table) 26 | - [`Column`](#column) 27 | - [`Binary`](#binary) 28 | - [`Char`](#char) 29 | - [`CString`](#cstring "`String` is a reserved word in PHP") 30 | - [`Integer`](#integer) 31 | - [`TinyInteger`](#tinyinteger) 32 | - [`SmallInteger`](#smallinteger) 33 | - [`MedumInteger`](#mediuminteger) 34 | - [`BigInteger`](#biginteger) 35 | - [`Increments`](#increments) 36 | - [`TinyIncrements`](#tinyincrements) 37 | - [`SmallIncrements`](#smallincrements) 38 | - [`MedumIncrements`](#mediumincrements) 39 | - [`CFloat`](#cfloat "`Float` is a reserved word in PHP") 40 | - [`Decimal`](#decimal) 41 | - [`DateTime`](#datetime) 42 | - [`DateTimeTz`](#datetimetz) 43 | - [`Time`](#time) 44 | - [`TimeTz`](#timetz) 45 | - [`Timestamp`](#timestamp) 46 | - [`TimestampTz`](#timestamptz) 47 | - [`Enum`](#enum) 48 | - [`Set`](#set) 49 | - [`Geometry`](#geometry) 50 | - [`Geography`](#geography) 51 | - [`Computed`](#computed) 52 | - [`Index`](#index) 53 | - [`Unique`](#unique) 54 | - [`Primary`](#primary) 55 | - [`Relationship`](#relationship) 56 | - [`ForeignKey`](#foreignkey) 57 | - [`PivotTable`](#pivottable) 58 | - [`PivotColumn`](#pivotcolumn) 59 | - [`Off`](#off) 60 | 61 | 62 | # Overview 63 | 64 | ## What It Is 65 | 66 | This package is a tool that creates Laravel migration files by inspecting the application's models with the command `php artisan implicit-migrations:generate`. Even after you change the model classes, you can run the command and generate a migration with the necessary update operations. 67 | 68 | ## How It Works 69 | 70 | With the most basic configuration, the `implicit-migrations:generate` artisan command looks at a Eloquent model and finds necessary information about the table properties such as the table name, primary key etc. Then it goes over the properties of the model and collects the name, type and default value information if provided. With the information collected, it creates a migration file and populates the `up()` and `down()` methods with the appropriate definitions. 71 | 72 | ### Implications 73 | 74 | For further details, the generator refers to some additional data in the model class which we call "Implications". These can be specified with either annotations or attributes on the class, its properties and methods. 75 | 76 | #### Annotations 77 | 78 | Annotations in DocBlocks with the format `@()` are recognized and interpreted as implications. For example, an annotation like this tells the generator that this integer property corresponds to an `UNSIGNED` `INT` column named `product_id` in the `order_items` table: 79 | 80 | ```php 81 | id(); 118 | $table->integer('product_id')->unsigned(); 119 | $table->timestamps(); 120 | } 121 | 122 | public function up(): void 123 | { 124 | Schema::create(static::TABLE_NAME, function (Blueprint $table) { 125 | $this->tableUp($table); 126 | }); 127 | } 128 | 129 | public function down(): void 130 | { 131 | Schema::drop(static::TABLE_NAME); 132 | } 133 | }; 134 | ``` 135 | 136 | You can find out more on other implications in the [Implication Reference](#implication-reference) section. 137 | 138 | 139 | #### PHP Attributes 140 | 141 | Another way of specifying implications is using PHP attributes. The very same implications are avaliable as attributes with the same notation. This is the same model definition as above as far as the generator is concerned: 142 | 143 | ```php 144 | integer('order_id'); 222 | } 223 | 224 | public function tableDown(Blueprint $table): void 225 | { 226 | $table->dropColumn('order_id'); 227 | } 228 | 229 | public function up(): void 230 | { 231 | Schema::table(static::TABLE_NAME, function (Blueprint $table) { 232 | $this->tableUp($table); 233 | }); 234 | } 235 | 236 | public function down(): void 237 | { 238 | Schema::table(static::TABLE_NAME, function (Blueprint $table) { 239 | $this->tableDown($table); 240 | }); 241 | } 242 | }; 243 | ``` 244 | 245 | 246 | # Installation 247 | 248 | The recommended installation is using `composer require --dev toramanlis/laravel-implicit-migrations` command. Since **this will make the implication attributes unavailable in production**, if you want to use attributes for implications you have to take one of the following approaches: 249 | 250 | ### Publishing The Attributes 251 | 252 | You can publish the attribute classes with `php artisan vendor:publish --tag=implication-attributes` and add the line `"database/attributes/composer.json"` in the `composer.json` file like this: 253 | 254 | ```json 255 | ... 256 | "extra": { 257 | "merge-plugin": { 258 | "include": [ 259 | "database/attributes/composer.json" 260 | ] 261 | } 262 | } 263 | ... 264 | ``` 265 | 266 | ### Opting Out From Attributes 267 | 268 | Each and every one of the implications are available as both attributes and annotations. You can completely give up using attributes and switch to the annotation notation with no missing functionality. 269 | 270 | 271 | ### Installing To Production 272 | 273 | Alternatively, you can always install the package with `composer install toramanlis/laravel-implicit-migrations` without the `--dev` option. Having a tool like this in production sure is unnecessary, but it's just that, unnecessary. 274 | 275 | 276 | # Configuration 277 | 278 | ## `database.model_paths` 279 | ##### Type: *`array`* 280 | ##### Default: *`['app/Models']`* 281 | 282 | An `array` of paths relative to the project directory where application models reside. If there are multiple model and migration paths in a project, the migration files are created in the migration path that is closest to the source model in the directory tree (complicit with [nWidart/laravel-modules](https://github.com/nWidart/laravel-modules)). 283 | 284 | ## `database.auto_infer_migrations` 285 | ##### Type: *`bool`* 286 | ##### Default: *`true`* 287 | 288 | This is a `boolean` value that controls, you guessed it, whether or not to infer the migration information automatically. What this means is basically, unless specified otherwise with an implication, none of the models, properties or methods are going to be inspected for migration information. However, if a property or method of a model has an implication, that model will be inspected. The default is `true`. 289 | 290 | ## `database.implications.` 291 | ##### Type: *`bool`* 292 | ##### Default: *`true`* 293 | 294 | These are `boolean` values that can be used to enable or disable each implication. The implication names have to be in snake case as per Laravel's convention for configuration keys e.g. `database.implications.foreign_key`. This set to `true` by default for all the implications. 295 | 296 | 297 | # Manual Migrations 298 | 299 | It's always a good idea to have a backup plan. You might come accross some more intricate or complicated requirements from a migration. For this reason, this tool doesn't take into account any migrations that does not have a `getSource()` method. This way, you can add your own custom migrations that are processed by Laravel's `migrate` command, but completely invisible to `implicit-migrations:generate`. 300 | 301 | If a manual migration happens to have a method named `getSource`, the [Off](#off) implication can be utilized to indicate that it is in fact a manual migration. 302 | 303 | 304 | # Implication Reference 305 | 306 | 307 | All the PHP attributes for the implications reside in the namespace `Toramanlis\ImplicitMigrations\Attributes`. If you choose to utilize them, make sure they're available in your production environment as well. See the [installation section](#installation) for details. 308 | 309 | Generally, the parameters of the implications are optional as they often have default values or can possibly be inferred from the rest of the information available in the application, such as the native PHP definitions of models, properties and methods or other implications' details. 310 | 311 | Best to keep in mind that these details still might not be sufficient to make a definition and some of the *optional* parameters might, in fact, be required. 312 | 313 | ## `Table` 314 | ##### Target: *`class`* 315 | 316 | `Table(?string $name = null, ?string $engine = null, ?string $charset = null, ?string $collation = null)` 317 | 318 | Used with classes for specifying the table details. When the `database.auto_infer_migrations` configuration option is set to `true`, using this implication lets the class get processed. 319 | 320 | 321 | ## `Column` 322 | 323 | ##### Target: *`class`*, *`property`* 324 | 325 | `Column(?string $type = null, ?string $name = null, ?bool $nullable = null, $default = null, ?int $length = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?int $precision = null, ?int $total = null, ?int $places = null, ?array $allowed = null, ?bool $fixed = null, ?string $subtype = null, ?int $srid = null, ?string $expression = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 326 | 327 | Can be used both on classes and properties to define columns. The `name` parameter is mandatory when used on classes as it won't be able to infer the column name. In contrast, when used on a property, column name defaults to the name of the property. Either by using it on a property or providing a `name` that matches a property allows it to infer whatever information available in the definition of said property. 328 | 329 | 330 | ## `Binary` 331 | 332 | ##### Target: *`class`*, *`property`* 333 | 334 | `Binary(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $fixed = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 335 | 336 | Alias for `Column('binary', $name, $nullable, $default, fixed: $fixed, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 337 | 338 | 339 | ## `Char` 340 | 341 | ##### Target: *`class`*, *`property`* 342 | 343 | `Char(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $length = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 344 | 345 | Alias for `Column('char', $name, $nullable, $default, $length, collation: $collation, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 346 | 347 | 348 | ## [`CString`](## "`String` is a reserved wordin PHP") 349 | 350 | ##### Target: *`class`*, *`property`* 351 | 352 | `CString(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $length = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 353 | 354 | Alias for `Column('string', $name, $nullable, $default, $length, collation: $collation, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 355 | 356 | 357 | ## `Integer` 358 | 359 | ##### Target: *`class`*, *`property`* 360 | 361 | `Integer(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 362 | 363 | Alias for `Column('integer', $name, $nullable, $default, unsigned: $unsigned, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 364 | 365 | 366 | ## `TinyInteger` 367 | 368 | ##### Target: *`class`*, *`property`* 369 | 370 | `TinyInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 371 | 372 | Alias for `Column('tinyInteger', $name, $nullable, $default, unsigned: $unsigned, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 373 | 374 | 375 | ## `SmallInteger` 376 | 377 | ##### Target: *`class`*, *`property`* 378 | 379 | `SmallInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 380 | 381 | Alias for `Column('smallInteger', $name, $nullable, $default, unsigned: $unsigned, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 382 | 383 | 384 | ## `MediumInteger` 385 | 386 | ##### Target: *`class`*, *`property`* 387 | 388 | `MediumInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 389 | 390 | Alias for `Column('mediumInteger', $name, $nullable, $default, unsigned: $unsigned, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 391 | 392 | 393 | ## `BigInteger` 394 | 395 | ##### Target: *`class`*, *`property`* 396 | 397 | `BigInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 398 | 399 | Alias for `Column('bigInteger', $name, $nullable, $default, unsigned: $unsigned, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 400 | 401 | 402 | ## `UnsignedInteger` 403 | 404 | ##### Target: *`class`*, *`property`* 405 | 406 | `UnsignedInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 407 | 408 | Alias for `Column('unsignedInteger', $name, $nullable, $default, unsigned: true, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 409 | 410 | 411 | ## `UnsignedTinyInteger` 412 | 413 | ##### Target: *`class`*, *`property`* 414 | 415 | `UnsignedTinyInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 416 | 417 | Alias for `Column('unsignedTinyInteger', $name, $nullable, $default, unsigned: true, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 418 | 419 | 420 | ## `UnsignedSmallInteger` 421 | 422 | ##### Target: *`class`*, *`property`* 423 | 424 | `UnsignedSmallInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 425 | 426 | Alias for `Column('unsignedSmallInteger', $name, $nullable, $default, unsigned: true, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 427 | 428 | 429 | ## `UnsignedMediumInteger` 430 | 431 | ##### Target: *`class`*, *`property`* 432 | 433 | `UnsignedMediumInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 434 | 435 | Alias for `Column('unsignedMediumInteger', $name, $nullable, $default, unsigned: true, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 436 | 437 | 438 | ## `UnsignedBigInteger` 439 | 440 | ##### Target: *`class`*, *`property`* 441 | 442 | `UnsignedBigInteger(protected ?string $name = null, ?bool $nullable = null, $default = null, ?bool $autoIncrement = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 443 | 444 | Alias for `Column('unsignedBigInteger', $name, $nullable, $default, unsigned: true, autoIncrement: $autoIncrement, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 445 | 446 | 447 | ## `Increments` 448 | 449 | ##### Target: *`class`*, *`property`* 450 | 451 | `Increments(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 452 | 453 | Alias for `Column('increments', $name, $nullable, $default, unsigned: true, autoIncrement: true, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 454 | 455 | 456 | ## `TinyIncrements` 457 | 458 | ##### Target: *`class`*, *`property`* 459 | 460 | `TinyIncrements(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 461 | 462 | Alias for `Column('tinyIncrements', $name, $nullable, $default, unsigned: true, autoIncrement: true, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 463 | 464 | 465 | ## `SmallIncrements` 466 | 467 | ##### Target: *`class`*, *`property`* 468 | 469 | `SmallIncrements(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 470 | 471 | Alias for `Column('smallIncrements', $name, $nullable, $default, unsigned: true, autoIncrement: true, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 472 | 473 | 474 | ## `MediumIncrements` 475 | 476 | ##### Target: *`class`*, *`property`* 477 | 478 | `MediumIncrements(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 479 | 480 | Alias for `Column('mediumIncrements', $name, $nullable, $default, unsigned: true, autoIncrement: true, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 481 | 482 | 483 | ## `BigIncrements` 484 | 485 | ##### Target: *`class`*, *`property`* 486 | 487 | `BigIncrements(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 488 | 489 | Alias for `Column('bigIncrements', $name, $nullable, $default, unsigned: true, autoIncrement: true, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 490 | 491 | 492 | ## [`CFloat`](## "`Float` is a reserved wordin PHP") 493 | 494 | ##### Target: *`class`*, *`property`* 495 | 496 | `CFloat(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 497 | 498 | Alias for `Column('float', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 499 | 500 | 501 | ## `Decimal` 502 | 503 | ##### Target: *`class`*, *`property`* 504 | 505 | `Decimal(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $total = null, ?int $places = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 506 | 507 | Alias for `Column('decimal', $name, $nullable, $default, total: $total, places: $places, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 508 | 509 | 510 | ## `DateTime` 511 | 512 | ##### Target: *`class`*, *`property`* 513 | 514 | `DateTime(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 515 | 516 | Alias for `Column('dateTime', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 517 | 518 | 519 | ## `DateTimeTz` 520 | 521 | ##### Target: *`class`*, *`property`* 522 | 523 | `DateTimeTz(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 524 | 525 | Alias for `Column('dateTimeTz', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 526 | 527 | 528 | ## `Time` 529 | 530 | ##### Target: *`class`*, *`property`* 531 | 532 | `Time(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 533 | 534 | Alias for `Column('time', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 535 | 536 | 537 | ## `TimeTz` 538 | 539 | ##### Target: *`class`*, *`property`* 540 | 541 | `TimeTz(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 542 | 543 | Alias for `Column('timeTz', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 544 | 545 | 546 | ## `Timestamp` 547 | 548 | ##### Target: *`class`*, *`property`* 549 | 550 | `Timestamp(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 551 | 552 | Alias for `Column('timestamp', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 553 | 554 | 555 | ## `TimestampTz` 556 | 557 | ##### Target: *`class`*, *`property`* 558 | 559 | `TimestampTz(protected ?string $name = null, ?bool $nullable = null, $default = null, ?int $precision = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 560 | 561 | Alias for `Column('timestampTz', $name, $nullable, $default, precision: $precision, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 562 | 563 | 564 | ## `Enum` 565 | 566 | ##### Target: *`class`*, *`property`* 567 | 568 | `Enum(protected ?string $name = null, ?bool $nullable = null, $default = null, ?array $allowed = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 569 | 570 | Alias for `Column('enum', $name, $nullable, $default, allowed: $allowed, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 571 | 572 | 573 | ## `Set` 574 | 575 | ##### Target: *`class`*, *`property`* 576 | 577 | `Set(protected ?string $name = null, ?bool $nullable = null, $default = null, ?array $allowed = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 578 | 579 | Alias for `Column('set', $name, $nullable, $default, allowed: $allowed, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 580 | 581 | 582 | ## `Geometry` 583 | 584 | ##### Target: *`class`*, *`property`* 585 | 586 | `Geometry(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $subtype = null, ?int $srid = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 587 | 588 | Alias for `Column('geometry', $name, $nullable, $default, subtype: $subtype, srid: $srid, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 589 | 590 | 591 | ## `Geography` 592 | 593 | ##### Target: *`class`*, *`property`* 594 | 595 | `Geography(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $subtype = null, ?int $srid = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 596 | 597 | Alias for `Column('geography', $name, $nullable, $default, subtype: $subtype, srid: $srid, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 598 | 599 | 600 | ## `Computed` 601 | 602 | ##### Target: *`class`*, *`property`* 603 | 604 | `Computed(protected ?string $name = null, ?bool $nullable = null, $default = null, ?string $expression = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 605 | 606 | Alias for `Column('computed', $name, $nullable, $default, expression: $expression, comment: $comment, virtualAs: $virtualAs, storedAs: $storedAs, after: $after)` 607 | 608 | 609 | ## `Index` 610 | 611 | ##### Target: *`class`*, *`property`* 612 | 613 | `Index(null|array|string $column = null, string $type = 'index', ?string $name = null, ?string $algorithm = null, ?string $language = null)` 614 | 615 | Just like `Column`, this can also be used both on classes and properties. The `column` parameter is optional when used on a property and defaults to the column name associated with that property even if the property doesn't have a `Column` implication of its own. When used on a class, the `column` parameter is mandatory. 616 | 617 | When `Index` is associated with a single column name by either using it on a property or having given a value to the `column` parameter, it will try and ensure the existence of a column with that name, using any information available in the model definition. 618 | 619 | 620 | ## `Unique` 621 | 622 | ##### Target: *`class`*, *`property`* 623 | 624 | `Unique(null|array|string $column = null, ?string $name = null, ?string $algorithm = null, ?string $language = null)` 625 | 626 | Alias for `Index($column, type: 'unique', ...$args)` 627 | 628 | 629 | ## `Primary` 630 | 631 | ##### Target: *`class`*, *`property`* 632 | 633 | `Primary(null|array|string $column = null, ?string $name = null, ?string $algorithm = null, ?string $language = null)` 634 | 635 | Alias for `Index($column, type: 'primary', ...$args)` 636 | 637 | 638 | ## `Relationship` 639 | 640 | ##### Target: *`method`* 641 | 642 | `Relationship()` 643 | 644 | Specifies that a method is a Laravel relationship. What kind of relationship it is will always be inferred by the return type of the method. This implication is redundant if the `database.auto_infer_migrations` configuration option is set to `true`, as the return type of a `public` method is already taken as an implication of whether or not it's a relationship method. 645 | 646 | If the type of relationship requires tables and columns that are not defined, `Relationship` will try to ensure them in the migration using whatever information is available. 647 | 648 | 649 | ## `ForeignKey` 650 | 651 | ##### Target: *`class`*, *`property`* 652 | 653 | `ForeignKey(string $on, null|array|string $column = null, null|array|string $references = null, ?string $onUpdate = null, ?string $onDelete = null)` 654 | 655 | Similar to `Index`, this can be used both on classes and properties, but with classes, it's mandatory to provide the `column` parameter. 656 | 657 | The `on` parameter can be a table name or a class name of a model. 658 | 659 | 660 | ## `PivotTable` 661 | 662 | ##### Target: *`method`* 663 | 664 | `PivotTable(?string $name = null, ?string $engine = null, ?string $charset = null, ?string $collation = null)` 665 | 666 | Specifies the details of a pivot table of a relationship. Even if no `Relationship` implication is present, having this implication lets the generator know it's a relationship method. 667 | 668 | 669 | ## `PivotColumn` 670 | 671 | ##### Target: *`method`* 672 | 673 | `PivotColumn(?string $name, protected ?string $type = null, ?bool $nullable = null, $default = null, ?int $length = null, ?bool $unsigned = null, ?bool $autoIncrement = null, ?int $precision = null, ?int $total = null, ?int $places = null, ?array $allowed = null, ?bool $fixed = null, ?string $subtype = null, ?int $srid = null, ?string $expression = null, ?string $collation = null, ?string $comment = null, ?string $virtualAs = null, ?string $storedAs = null, ?string $after = null)` 674 | 675 | Defines a column on a pivot table of a relationship. Just like [`PivotTable`](#pivottable), having this implication lets the generator know it's a relationship method. Since pivot tables typically don't have models of their own, we define any **extra** columns on the relationship method they are required by. Only the columns other than the foreign keys need this implicaiton, foreign keys are already covered with the relationship. It's still allowed to use this implication to fine tune them, though. 676 | 677 | 678 | ## `Off` 679 | 680 | ##### Target: *`class`*, *`property`*, *`method`* 681 | 682 | `Off()` 683 | 684 | Lets the generator know that the given class, property or method should be ignored. This includes `getSource` method a migration. If you have a manually written migration that happens to have a method named `getSource`, you can add this implication to that method to keep the generator off of that migration. 685 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "toramanlis/laravel-implicit-migrations", 3 | "description": "This package is a tool that creates Laravel migration files by inspecting the application's models.", 4 | "keywords": [ 5 | "laravel", 6 | "eloquent", 7 | "model", 8 | "migration", 9 | "dev" 10 | ], 11 | "type": "library", 12 | "require": { 13 | "laravel/framework": "~12" 14 | }, 15 | "license": "MIT", 16 | "autoload": { 17 | "psr-4": { 18 | "Toramanlis\\ImplicitMigrations\\": "src/" 19 | } 20 | }, 21 | "autoload-dev": { 22 | "psr-4": { 23 | "Toramanlis\\Tests\\": "tests/", 24 | "Toramanlis\\Tests\\Data\\": "tests/_data/" 25 | } 26 | }, 27 | "authors": [ 28 | { 29 | "name": "Timucin Bahsi", 30 | "email": "timucinbahsi@gmail.com" 31 | } 32 | ], 33 | "extra": { 34 | "laravel": { 35 | "providers": [ 36 | "Toramanlis\\ImplicitMigrations\\Providers\\ImplicitMigrationsServiceProvider" 37 | ] 38 | } 39 | }, 40 | "require-dev": { 41 | "orchestra/testbench": "^10.1", 42 | "laravel/sail": "^1.41", 43 | "squizlabs/php_codesniffer": "^3.12" 44 | }, 45 | "prefer-stable": true, 46 | "minimum-stability": "stable", 47 | "funding": [ 48 | { 49 | "type": "patreon", 50 | "url": "https://www.patreon.com/toramanlis" 51 | } 52 | ] 53 | } -------------------------------------------------------------------------------- /src/Attributes/Applicable.php: -------------------------------------------------------------------------------- 1 | enabled()) { 16 | return $table; 17 | } 18 | 19 | return $this->process($table); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Attributes/BigIncrements.php: -------------------------------------------------------------------------------- 1 | 'json', 20 | 'bool' => 'boolean', 21 | 'float' => 'decimal', 22 | 'int' => 'integer', 23 | 'object' => 'json', 24 | 'string' => 'string', 25 | 'iterable' => 'json', 26 | ]; 27 | 28 | public const PARAMETER_MAP = [ 29 | 'char' => ['length'], 30 | 'string' => ['length'], 31 | 'integer' => ['autoIncrement', 'unsigned'], 32 | 'tinyInteger' => ['autoIncrement', 'unsigned'], 33 | 'smallInteger' => ['autoIncrement', 'unsigned'], 34 | 'mediumInteger' => ['autoIncrement', 'unsigned'], 35 | 'bigInteger' => ['autoIncrement', 'unsigned'], 36 | 'float' => ['precision'], 37 | 'decimal' => ['total', 'places'], 38 | 'enum' => ['allowed'], 39 | 'set' => ['allowed'], 40 | 'dateTime' => ['precision'], 41 | 'dateTimeTz' => ['precision'], 42 | 'time' => ['precision'], 43 | 'timeTz' => ['precision'], 44 | 'timestamp' => ['precision'], 45 | 'timestampTz' => ['precision'], 46 | 'binary' => ['length', 'fixed'], 47 | 'geometry' => ['subtype', 'srid'], 48 | 'geography' => ['subtype', 'srid'], 49 | 'computed' => ['expression'], 50 | ]; 51 | 52 | public const SUPPORTED_MODIFIERS = [ 53 | 'after', 54 | 'autoIncrement', 55 | 'charset', 56 | 'collation', 57 | 'comment', 58 | 'default', 59 | 'first', 60 | 'from', 61 | 'invisible', 62 | 'nullable', 63 | 'storedAs', 64 | 'unsigned', 65 | 'useCurrent', 66 | 'useCurrentOnUpdate', 67 | 'virtualAs', 68 | 'generatedAs', 69 | 'always', 70 | ]; 71 | 72 | public const SUPPORTED_ATTRIBUTES = [ 73 | 'after', 74 | 'autoIncrement', 75 | 'comment', 76 | 'default', 77 | 'nullable', 78 | 'storedAs', 79 | 'unsigned', 80 | 'virtualAs', 81 | 'length', 82 | 'precision', 83 | 'total', 84 | 'places', 85 | 'allowed', 86 | 'fixed', 87 | 'subtype', 88 | 'srid', 89 | 'expression', 90 | 'collation', 91 | ]; 92 | 93 | protected bool $inferred = false; 94 | 95 | public function __construct( 96 | protected ?string $type = null, 97 | protected ?string $name = null, 98 | protected ?bool $nullable = null, 99 | protected $default = null, 100 | protected ?int $length = null, 101 | protected ?bool $unsigned = null, 102 | protected ?bool $autoIncrement = null, 103 | protected ?int $precision = null, 104 | protected ?int $total = null, 105 | protected ?int $places = null, 106 | protected ?array $allowed = null, 107 | protected ?bool $fixed = null, 108 | protected ?string $subtype = null, 109 | protected ?int $srid = null, 110 | protected ?string $expression = null, 111 | protected ?string $collation = null, 112 | protected ?string $comment = null, 113 | protected ?string $virtualAs = null, 114 | protected ?string $storedAs = null, 115 | protected ?string $after = null 116 | ) { 117 | } 118 | 119 | public function setInferred(bool $value = true) 120 | { 121 | $this->inferred = $value; 122 | } 123 | 124 | public function inferFromReflectionProperty(ReflectionProperty $reflection): void 125 | { 126 | $this->name = Str::snake($reflection->getName()); 127 | 128 | if ( 129 | null !== $this->type && 130 | null !== $this->nullable && 131 | null !== $this->default 132 | ) { 133 | return; 134 | } 135 | 136 | if (null === $this->default && $reflection->hasDefaultValue()) { 137 | $this->default = $reflection->getDefaultValue(); 138 | } 139 | 140 | if (!$reflection->hasType()) { 141 | return; 142 | } 143 | 144 | /** @var ReflectionType */ 145 | $reflectionType = $reflection->getType(); 146 | 147 | if (null === $this->type && $reflectionType instanceof ReflectionNamedType) { 148 | $this->type = static::TYPE_MAP[$reflectionType->getName()] ?? null; 149 | } 150 | 151 | if (null === $this->nullable) { 152 | $this->nullable = $reflectionType->allowsNull() ?: null; 153 | } 154 | } 155 | 156 | protected function validate(Blueprint $table) 157 | { 158 | if (empty($this->name)) { 159 | throw new Exception(Exception::CODE_COL_NO_NAME, [$table->getTable()]); 160 | } 161 | 162 | if (null === $this->type) { 163 | throw new Exception(Exception::CODE_COL_NO_TYPE, [$table->getTable(), $this->name]); 164 | } 165 | } 166 | 167 | public static function getParameters($type, $attributes): array 168 | { 169 | $parameters = []; 170 | foreach (static::PARAMETER_MAP[$type] ?? [] as $parameterName) { 171 | if (!array_key_exists($parameterName, $attributes)) { 172 | continue; 173 | } 174 | 175 | $parameters[$parameterName] = $attributes[$parameterName]; 176 | } 177 | 178 | return $parameters; 179 | } 180 | 181 | protected function process(Blueprint $table): Blueprint 182 | { 183 | try { 184 | $this->validate($table); 185 | } catch (Exception $e) { 186 | if ($this->inferred) { 187 | return $table; 188 | } 189 | 190 | throw $e; 191 | } catch (BaseException $e) { 192 | $reportedName = $this->name ?? '???'; 193 | throw new Exception( 194 | Exception::CODE_COL_GENERIC, 195 | [$table->getTable(), $reportedName], 196 | $e 197 | ); 198 | } 199 | 200 | $attributes = []; 201 | 202 | foreach (static::SUPPORTED_ATTRIBUTES as $attributeName) { 203 | if (null === $this->{$attributeName}) { 204 | continue; 205 | } 206 | 207 | $attributes[$attributeName] = $this->{$attributeName}; 208 | } 209 | 210 | $parameters = static::getParameters($this->type, $attributes); 211 | 212 | if ( 213 | !empty(array_diff( 214 | array_keys($attributes), 215 | static::SUPPORTED_MODIFIERS, 216 | array_keys($parameters) 217 | )) 218 | ) { 219 | $table->addColumn($this->type, $this->name, $attributes); 220 | return $table; 221 | } 222 | 223 | $column = $table->{$this->type}($this->name, ...$parameters); 224 | 225 | foreach (static::SUPPORTED_MODIFIERS as $modifier) { 226 | if (!isset($attributes[$modifier])) { 227 | continue; 228 | } 229 | 230 | if (false !== $attributes[$modifier]) { 231 | $column->$modifier($attributes[$modifier]); 232 | } 233 | } 234 | 235 | return $table; 236 | } 237 | } 238 | -------------------------------------------------------------------------------- /src/Attributes/ColumnAlias.php: -------------------------------------------------------------------------------- 1 | '', 40 | 1 => 'Cannot create a column without name: %s.???', 41 | 2 => 'Cannot create a column without type: %s.%s', 42 | 3 => 'There was an error while processing a column: %s.%s', 43 | 4 => 'Cannot detect the referenced model for foreign key: %s', 44 | 5 => 'There was an error while detecting a foreign key\'s column: %s on table: %s', 45 | 6 => 'Invalid type for index: %s', 46 | 7 => 'Cannot create an index without columns: %s.%s', 47 | 8 => 'Unknown relationship type: %s', 48 | 9 => 'Unable to detect pivot table for relationship', 49 | 10 => 'Unable to detect parent table for relationship', 50 | 11 => 'Unable to detect related table for relationship', 51 | 12 => 'Unable to detect foreign key for relationship', 52 | 13 => 'Unable to detect local key for relationship', 53 | ]; 54 | 55 | public function __construct(int $code = self::CODE_NONE, array $context = [], ?Throwable $previous = null) 56 | { 57 | $code = isset(static::MESSAGES[$code]) ? $code : self::CODE_NONE; 58 | $message = sprintf(static::MESSAGES[$code], ...$context); 59 | 60 | parent::__construct($message, $code, $previous); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Attributes/ForeignKey.php: -------------------------------------------------------------------------------- 1 | on = $instance; 36 | } else { 37 | $this->on = $on; 38 | } 39 | 40 | $this->columns = is_string($column) ? [$column] : $column; 41 | $this->references = is_string($references) ? [$references] : $references; 42 | } 43 | 44 | public function getReferenceTableName() 45 | { 46 | if (null === $this->referenceTableName) { 47 | if (is_a($this->on, Model::class, true)) { 48 | $on = is_string($this->on) ? (new $this->on()) : $this->on; 49 | $this->referenceTableName = $on->getTable(); 50 | } else { 51 | $this->referenceTableName = $this->on; 52 | } 53 | } 54 | 55 | return $this->referenceTableName; 56 | } 57 | 58 | public function inferFromReflectionProperty(ReflectionProperty $reflection): void 59 | { 60 | if (null !== $this->columns && null !== $this->references) { 61 | return; 62 | } 63 | 64 | if (null === $this->columns) { 65 | $this->columns = [Str::snake($reflection->getName())]; 66 | } 67 | 68 | $this->inferFromReflectionClass($reflection->getDeclaringClass()); 69 | } 70 | 71 | public function inferFromReflectionClass(ReflectionClass $reflection): void 72 | { 73 | if ( 74 | (null !== $this->references && null !== $this->columns) || 75 | !is_a($this->on, Model::class) 76 | ) { 77 | return; 78 | } 79 | 80 | $this->columns = $this->columns ?? [$this->on->getForeignKey()]; 81 | $this->references = $this->references ?? [$this->on->getKeyName()]; 82 | } 83 | 84 | public function inferFromExistingData(): void 85 | { 86 | if (null !== $this->columns) { 87 | return; 88 | } 89 | 90 | $this->columns = [Str::snake(Str::singular($this->getReferenceTableName())) . '_id']; 91 | } 92 | 93 | protected function getReferencedModelName(array $modelNames): string 94 | { 95 | if (is_a($this->on, Model::class)) { 96 | return $this->on::class; 97 | } 98 | 99 | foreach ($modelNames as $modelName) { 100 | if ((new $modelName())->getTable() === $this->getReferenceTableName()) { 101 | return $modelName; 102 | } 103 | } 104 | 105 | throw new Exception(Exception::CODE_FK_NO_MODEL, [$this->columns[0]]); 106 | } 107 | 108 | protected function ensureColumn($columnName, Blueprint $table, array $blueprints, array $modelNames): void 109 | { 110 | foreach ($table->getColumns() as $column) { 111 | if ($column->name === $columnName) { 112 | return; 113 | } 114 | } 115 | 116 | try { 117 | $parameters = []; 118 | $referencedTableName = $this->getReferenceTableName(); 119 | 120 | if (!isset($blueprints[$referencedTableName])) { 121 | throw new Exception(); 122 | } 123 | 124 | /** @var Blueprint */ 125 | $referencedTable = $blueprints[$referencedTableName]; 126 | try { 127 | foreach ($referencedTable->getColumns() as $column) { 128 | if ($column->name !== $this->references[0]) { 129 | continue; 130 | } 131 | 132 | $parameters = $column->getAttributes(); 133 | unset( 134 | $parameters['name'], 135 | $parameters['type'], 136 | $parameters['autoIncrement'], 137 | $parameters['primary'], 138 | $parameters['index'], 139 | $parameters['unique'] 140 | ); 141 | $table->addColumn($column->type, $columnName, $parameters); 142 | return; 143 | } 144 | 145 | throw new Exception(); 146 | } catch (Exception $e) { 147 | $propertyReflection = new ReflectionProperty( 148 | $this->getReferencedModelName($modelNames), 149 | $this->references[0] 150 | ); 151 | 152 | $propertyType = $propertyReflection->getType(); 153 | 154 | $type = $propertyType ? Column::TYPE_MAP[$propertyType->getName()] : 'unsignedBigInteger'; 155 | 156 | $table->$type($columnName); 157 | $referencedTable->$type($this->references[0]); 158 | return; 159 | } 160 | } catch (Exception $e) { 161 | $table->unsignedBigInteger($columnName); 162 | } catch (BaseException $e) { 163 | throw new Exception( 164 | Exception::CODE_FK_NO_COL, 165 | [$this->columns[0], $table->getTable()], 166 | $e 167 | ); 168 | } 169 | } 170 | 171 | public function ensureColumns(Blueprint $table, array $blueprints, array $modelNames): void 172 | { 173 | if (count($this->columns ?? []) !== 1) { 174 | foreach ($this->columns ?? [] as $column) { 175 | $this->ensureColumn($column, $table, $blueprints, $modelNames); 176 | } 177 | 178 | return; 179 | } 180 | 181 | $columnName = $this->columns[0]; 182 | 183 | $this->ensureColumn($columnName, $table, $blueprints, $modelNames); 184 | } 185 | 186 | protected function process(Blueprint $table): Blueprint 187 | { 188 | $this->references = empty($this->references) ? ['id'] : $this->references; 189 | 190 | $references = is_array($this->references) && count($this->references) === 1 191 | ? $this->references[0] 192 | : $this->references; 193 | 194 | $table->foreign($this->columns) 195 | ->references($references) 196 | ->on($this->getReferenceTableName()) 197 | ->onUpdate($this->onUpdate) 198 | ->onDelete($this->onDelete); 199 | 200 | return $table; 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Attributes/Geography.php: -------------------------------------------------------------------------------- 1 | type = IndexType::from(strtolower($type)); 31 | } catch (ValueError $e) { 32 | throw new Exception(Exception::CODE_IDX_NO_TYPE, [$type], $e); 33 | } 34 | 35 | $this->columns = is_string($column) ? [$column] : $column; 36 | } 37 | 38 | public function inferFromReflectionProperty(ReflectionProperty $reflection): void 39 | { 40 | if (null !== $this->columns) { 41 | return; 42 | } 43 | 44 | $this->columns = [Str::snake($reflection->getName())]; 45 | } 46 | 47 | public function ensureColumns(Blueprint $table): void 48 | { 49 | if (is_null($this->columns)) { 50 | return; 51 | } 52 | 53 | $existingColumns = array_map(fn ($column) => $column->name, $table->getColumns()); 54 | $missingColumns = []; 55 | foreach ($this->columns as $column) { 56 | if (!in_array($column, $existingColumns)) { 57 | $missingColumns[] = $column; 58 | } 59 | } 60 | 61 | if (empty($missingColumns)) { 62 | return; 63 | } 64 | 65 | foreach ($missingColumns as $column) { 66 | $table->string($column); 67 | } 68 | } 69 | 70 | protected function validate(Blueprint $table) 71 | { 72 | if (empty($this->columns)) { 73 | throw new Exception( 74 | Exception::CODE_IDX_NO_COL, 75 | [$table->getTable(), $this->type->name] 76 | ); 77 | } 78 | } 79 | 80 | protected function process(Blueprint $table): Blueprint 81 | { 82 | $this->validate($table); 83 | 84 | /** @var IndexDefinition $index */ 85 | $index = $table->{$this->type->value}($this->columns, $this->name); 86 | $index->algorithm($this->algorithm); 87 | $index->language($this->language); 88 | 89 | return $table; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Attributes/IndexType.php: -------------------------------------------------------------------------------- 1 | name ?? ''; 60 | } 61 | 62 | public function inferFromReflectionProperty(ReflectionProperty $reflection): void 63 | { 64 | return; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Attributes/PivotTable.php: -------------------------------------------------------------------------------- 1 | name) { 28 | return; 29 | } 30 | 31 | $modelClass = $reflection->getName(); 32 | 33 | /** @var Model */ 34 | $model = new $modelClass(); 35 | $this->name = $model->getTable(); 36 | } 37 | 38 | protected function process(Blueprint $table): Blueprint 39 | { 40 | $table->engine($this->engine); 41 | $table->charset($this->charset); 42 | $table->collation($this->collation); 43 | 44 | return $table; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Attributes/Time.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | public array $modifiedColumns; 18 | 19 | /** 20 | * @param array $droppedColumns 21 | */ 22 | public array $droppedColumns; 23 | 24 | /** 25 | * @param array $addedColumns 26 | */ 27 | public array $addedColumns; 28 | 29 | /** 30 | * @param array $droppedIndexes 31 | */ 32 | public array $droppedIndexes; 33 | 34 | /** 35 | * @param array $renamedIndexes 36 | */ 37 | public array $renamedIndexes; 38 | 39 | /** 40 | * @param array $addedIndexes 41 | */ 42 | public array $addedIndexes; 43 | 44 | /** 45 | * @param SimplifyingBlueprint $from 46 | * @param SimplifyingBlueprint $to 47 | */ 48 | public function __construct( 49 | readonly public SimplifyingBlueprint $from, 50 | readonly public SimplifyingBlueprint $to 51 | ) { 52 | [$this->modifiedColumns, $this->droppedColumns, $this->addedColumns] = static::getColumnDiffs($from, $to); 53 | [$this->droppedIndexes, $this->renamedIndexes, $this->addedIndexes] = static::getIndexDiffs($from, $to); 54 | } 55 | 56 | public function applyColumnIndexes(bool $reverse = false) 57 | { 58 | $original = $reverse ? $this->from : $this->to; 59 | 60 | /** @var SimplifyingBlueprint */ 61 | $blueprint = App::make(SimplifyingBlueprint::class, ['tableName' => ($original)->getTable()]); 62 | 63 | foreach ($this->getAddedColumns($reverse) as $column) { 64 | $blueprint->addColumn($column->type, $column->name, $column->getAttributes()); 65 | } 66 | 67 | foreach ($this->getAddedIndexes($reverse) as $index) { 68 | $addedIndex = $blueprint->{$index->name}($index->columns, $this->indexName($index), $index->algorithm); 69 | 70 | foreach (array_keys($index->getAttributes()) as $attribute) { 71 | $addedIndex->{$attribute} = $index->{$attribute}; 72 | } 73 | } 74 | 75 | $blueprint->applyColumnIndexes(); 76 | 77 | if ($reverse) { 78 | $this->droppedColumns = $blueprint->getColumns(); 79 | $this->droppedIndexes = []; 80 | $addedIndexes = &$this->droppedIndexes; 81 | } else { 82 | $this->addedColumns = $blueprint->getColumns(); 83 | $this->addedIndexes = []; 84 | $addedIndexes = &$this->addedIndexes; 85 | } 86 | 87 | foreach ($blueprint->getCommands() as $command) { 88 | if (null === IndexType::tryFrom($command->name)) { 89 | continue; 90 | } 91 | 92 | $addedIndexes[] = $command; 93 | } 94 | } 95 | 96 | public function none() 97 | { 98 | return empty($this->modifiedColumns) && 99 | empty($this->droppedColumns) && 100 | empty($this->renamedColumns) && 101 | empty($this->addedColumns) && 102 | empty($this->droppedIndexes) && 103 | empty($this->renamedIndexes) && 104 | empty($this->addedIndexes) && 105 | null === $this->getEngineChange() && 106 | null === $this->getCharsetChange() && 107 | null === $this->getCollationChange() && 108 | null === $this->getRename(); 109 | } 110 | 111 | public function getAddedColumns(bool $reverse = false) 112 | { 113 | return $reverse ? $this->droppedColumns : $this->addedColumns; 114 | } 115 | 116 | public function getDroppedColumns(bool $reverse = false) 117 | { 118 | return $reverse ? $this->addedColumns : $this->droppedColumns; 119 | } 120 | 121 | public function getModifiedColumns(bool $reverse = false): array 122 | { 123 | $reference = $reverse ? $this->from : $this->to; 124 | 125 | $modifiedColumns = []; 126 | 127 | foreach ($reference->getColumns() as $column) { 128 | if (!in_array($column->name, $this->modifiedColumns)) { 129 | continue; 130 | } 131 | 132 | $modifiedColumns[] = $column; 133 | } 134 | 135 | return $modifiedColumns; 136 | } 137 | 138 | public function getRenamedIndexes(bool $reverse = false) 139 | { 140 | $renames = $this->renamedIndexes; 141 | return $reverse ? array_flip($renames) : $renames; 142 | } 143 | 144 | public function getAddedIndexes(bool $reverse = false) 145 | { 146 | return $reverse ? $this->droppedIndexes : $this->addedIndexes; 147 | } 148 | 149 | public function getDroppedIndexes(bool $reverse = false) 150 | { 151 | return $reverse ? $this->addedIndexes : $this->droppedIndexes; 152 | } 153 | 154 | public function getOptionChange(string $optionName, bool $reverse = false): ?string 155 | { 156 | if ($this->from->$optionName === $this->to->$optionName) { 157 | return null; 158 | } 159 | 160 | return $reverse ? $this->from->$optionName : $this->to->$optionName; 161 | } 162 | 163 | public function getEngineChange(bool $reverse = false) 164 | { 165 | return $this->getOptionChange('engine', $reverse); 166 | } 167 | 168 | public function getCharsetChange(bool $reverse = false) 169 | { 170 | return $this->getOptionChange('charset', $reverse); 171 | } 172 | 173 | public function getCollationChange(bool $reverse = false) 174 | { 175 | return $this->getOptionChange('collation', $reverse); 176 | } 177 | 178 | public function getRename(bool $reverse = false) 179 | { 180 | if ($this->from->getTable() === $this->to->getTable()) { 181 | return null; 182 | } 183 | 184 | $rename = [$this->from->getTable(), $this->to->getTable()]; 185 | return $reverse ? array_reverse($rename) : $rename; 186 | } 187 | 188 | public function getDependedColumnNames(): array 189 | { 190 | $dependedColumns = []; 191 | foreach ($this->addedIndexes as $index) { 192 | if (IndexType::Foreign->value !== $index->name) { 193 | continue; 194 | } 195 | 196 | $references = is_array($index->references) ? $index->references : [$index->references]; 197 | foreach ($references as $reference) { 198 | $dependedColumns[] = "{$index->on}.{$reference}"; 199 | } 200 | } 201 | 202 | return $dependedColumns; 203 | } 204 | 205 | public function getAddedColumnNames(): array 206 | { 207 | return array_map(fn ($column) => $column->name, $this->addedColumns); 208 | } 209 | 210 | public function extractForeignKey(string $on, string $reference): Fluent 211 | { 212 | foreach ($this->addedIndexes as $index) { 213 | $references = is_array($index->references) ? $index->references : [$index->references]; 214 | if ( 215 | IndexType::Foreign->value !== $index->name || 216 | $index->on !== $on || 217 | !in_array($reference, $references) 218 | ) { 219 | continue; 220 | } 221 | 222 | $this->addedIndexes = array_filter( 223 | $this->addedIndexes, 224 | fn($i) => $this->indexName($i) !== $this->indexName($index) 225 | ); 226 | return $index; 227 | } 228 | 229 | throw new Exception("Reference {$on}.{$reference} has no foreign key in blueprint for {$this->to->getTable()}"); 230 | } 231 | 232 | public function dropAddedIndex($indexName, bool $reverse = false) 233 | { 234 | $remaining = []; 235 | 236 | foreach ($this->getAddedIndexes($reverse) as $index) { 237 | if ($this->indexName($index) === $indexName) { 238 | continue; 239 | } 240 | 241 | $remaining[] = $index; 242 | } 243 | 244 | if ($reverse) { 245 | $this->droppedIndexes = $remaining; 246 | } else { 247 | $this->addedIndexes = $remaining; 248 | } 249 | } 250 | 251 | public function stripDefaultIndexNames(bool $reverse = false) 252 | { 253 | $blueprint = $reverse ? $this->from : $this->to; 254 | foreach ($this->getAddedIndexes($reverse) as $index) { 255 | if ($blueprint->defaultIndexName($index) === $index->index) { 256 | $index->index = null; 257 | } 258 | } 259 | } 260 | 261 | public function indexName(Fluent $index, bool $reverse = false) 262 | { 263 | return $index->index ?? $this->defaultIndexName($index, $reverse); 264 | } 265 | 266 | public function defaultIndexName(Fluent $index, bool $reverse = false) 267 | { 268 | return ($reverse ? $this->from : $this->to)->defaultIndexName($index); 269 | } 270 | 271 | protected static function attributesEqual(Fluent $left, Fluent $right, array $exceptions = []) 272 | { 273 | $leftClone = clone $left; 274 | $rightClone = clone $right; 275 | 276 | $leftAttributes = array_filter($leftClone->getAttributes(), fn ($i) => null !== $i); 277 | $rightAttributes = array_filter($rightClone->getAttributes(), fn ($i) => null !== $i); 278 | 279 | ksort($leftAttributes); 280 | ksort($rightAttributes); 281 | 282 | foreach ($exceptions as $exception) { 283 | unset($leftAttributes[$exception], $rightAttributes[$exception]); 284 | } 285 | 286 | return $leftAttributes === $rightAttributes; 287 | } 288 | 289 | /** 290 | * @param Blueprint $from 291 | * @param Blueprint $to 292 | * @return array 293 | */ 294 | protected static function getColumnDiffs(Blueprint $from, Blueprint $to): array 295 | { 296 | $unchangedColumns = []; 297 | $modifiedColumns = []; 298 | $droppedColumns = []; 299 | $addedColumns = []; 300 | 301 | foreach ($from->getColumns() as $fromColumn) { 302 | foreach ($to->getColumns() as $toColumn) { 303 | if (in_array($toColumn->name, $unchangedColumns)) { 304 | continue; 305 | } 306 | 307 | if ($fromColumn->name === $toColumn->name) { 308 | if (static::attributesEqual($fromColumn, $toColumn)) { 309 | $unchangedColumns[] = $fromColumn->name; 310 | continue 2; 311 | } 312 | 313 | $modifiedColumns[] = $fromColumn->name; 314 | continue 2; 315 | } 316 | } 317 | 318 | $droppedColumns[] = $fromColumn; 319 | } 320 | 321 | foreach ($to->getColumns() as $toColumn) { 322 | if ( 323 | in_array($toColumn->name, $unchangedColumns) || 324 | in_array($toColumn->name, $modifiedColumns) 325 | ) { 326 | continue; 327 | } 328 | 329 | $addedColumns[] = $toColumn; 330 | } 331 | 332 | return [$modifiedColumns, $droppedColumns, $addedColumns]; 333 | } 334 | 335 | /** 336 | * @param Blueprint $from 337 | * @param Blueprint $to 338 | * @return array 339 | */ 340 | protected static function getIndexDiffs(Blueprint $from, Blueprint $to): array 341 | { 342 | $unchangedIndexes = []; 343 | $droppedIndexes = []; 344 | $renamedIndexes = []; 345 | $addedIndexes = []; 346 | 347 | foreach ($from->getCommands() as $fromCommand) { 348 | if (null === IndexType::tryFrom($fromCommand->name)) { 349 | continue; 350 | } 351 | 352 | foreach ($to->getCommands() as $toCommand) { 353 | if ( 354 | null === IndexType::tryFrom($toCommand->name) || 355 | in_array($toCommand->index, $unchangedIndexes) 356 | ) { 357 | continue; 358 | } 359 | 360 | if (static::attributesEqual($fromCommand, $toCommand, ['index'])) { 361 | if ($fromCommand->index === $toCommand->index) { 362 | unset($renamedIndexes[$fromCommand->index]); 363 | $unchangedIndexes[] = $fromCommand->index; 364 | continue 2; 365 | } 366 | 367 | $renamedIndexes[$fromCommand->index] = $toCommand->index; 368 | continue 2; 369 | } 370 | } 371 | 372 | $droppedIndexes[] = $fromCommand; 373 | } 374 | 375 | foreach ($to->getCommands() as $toCommand) { 376 | if (null === IndexType::tryFrom($toCommand->name)) { 377 | continue; 378 | } 379 | 380 | if ( 381 | in_array($toCommand->index, $unchangedIndexes) || 382 | in_array($toCommand->index, $renamedIndexes) 383 | ) { 384 | continue; 385 | } 386 | 387 | $addedIndexes[] = $toCommand; 388 | } 389 | 390 | return [$droppedIndexes, $renamedIndexes, $addedIndexes]; 391 | } 392 | } 393 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/ColumnDiffExporter.php: -------------------------------------------------------------------------------- 1 | change()'; 10 | return parent::joinModifiers($modifiers); 11 | } 12 | 13 | /** 14 | * @param string $from 15 | * @param string $to 16 | * @return string 17 | */ 18 | public static function renameColumn(string $from, string $to) 19 | { 20 | $parameters = static::exportParameters([$from, $to]); 21 | return "\$table->renameColumn({$parameters});"; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/ColumnExporter.php: -------------------------------------------------------------------------------- 1 | */ 18 | protected array $attributes; 19 | 20 | /** @var array */ 21 | protected array $collapsedAttributes; 22 | 23 | protected ?string $collapsedType = null; 24 | 25 | public ?Fluent $foreignKey = null; 26 | 27 | public const SUPPORTED_MODIFIERS = Column::SUPPORTED_MODIFIERS; 28 | 29 | public function __construct(public readonly ColumnDefinition $definition) 30 | { 31 | $this->attributes = array_filter($this->definition->getAttributes()); 32 | unset( 33 | $this->attributes['type'], 34 | $this->attributes['name'], 35 | $this->attributes['change'], 36 | $this->attributes[IndexType::Primary->value], 37 | $this->attributes[IndexType::Unique->value], 38 | $this->attributes[IndexType::Index->value] 39 | ); 40 | 41 | $this->runCollapsables(); 42 | } 43 | 44 | public function setForeignKey(Fluent $foreignKey) 45 | { 46 | if ('unsignedBigInteger' !== $this->collapsedType || !empty($this->collapsedAttributes)) { 47 | return false; 48 | } 49 | 50 | $foreignKey->references = 'id' === $foreignKey->references ? null : $foreignKey->references; 51 | $this->foreignKey = $foreignKey; 52 | return true; 53 | } 54 | 55 | protected function buildIndexModifiers() 56 | { 57 | $modifiers = []; 58 | 59 | foreach ([IndexType::Primary->value, IndexType::Unique->value, IndexType::Index->value] as $indexType) { 60 | if (!$this->definition->$indexType) { 61 | continue; 62 | } 63 | 64 | $parameters = true === $this->definition->$indexType ? [] : [$this->definition->$indexType]; 65 | $modifiers[] = static::makeModifier($indexType, $parameters); 66 | } 67 | 68 | return $modifiers; 69 | } 70 | 71 | protected static function getParameterDefaults($type) 72 | { 73 | if (isset(static::$parameterDefaults[$type])) { 74 | return static::$parameterDefaults[$type]; 75 | } 76 | 77 | switch ($type) { 78 | case 'char': 79 | case 'string': 80 | static::$parameterDefaults['string'] = ['length' => Builder::$defaultStringLength]; 81 | return static::$parameterDefaults['string']; 82 | case 'time': 83 | case 'timeTz': 84 | case 'timestamp': 85 | case 'timestampTz': 86 | case 'dateTime': 87 | case 'dateTimeTz': 88 | static::$parameterDefaults[$type] = ['precision' => Builder::$defaultTimePrecision]; 89 | return static::$parameterDefaults[$type]; 90 | case 'float': 91 | $argumentNames = ['precision']; 92 | break; 93 | case 'decimal': 94 | $argumentNames = ['total', 'places']; 95 | break; 96 | case 'geography': 97 | $argumentNames = ['subtype', 'srid']; 98 | break; 99 | default: 100 | static::$parameterDefaults[$type] = []; 101 | return static::$parameterDefaults[$type]; 102 | } 103 | 104 | $reflectionMethod = new ReflectionMethod(Blueprint::class, $type); 105 | $reflectionParameters = $reflectionMethod->getParameters(); 106 | 107 | static::$parameterDefaults[$type] = []; 108 | foreach ($reflectionParameters as $reflectionParameter) { 109 | $argumentName = $reflectionParameter->getName(); 110 | 111 | if (!$reflectionParameter->isDefaultValueAvailable() || !in_array($argumentName, $argumentNames)) { 112 | continue; 113 | } 114 | 115 | static::$parameterDefaults[$type][$argumentName] = $reflectionParameter->getDefaultValue(); 116 | } 117 | 118 | return static::$parameterDefaults[$type]; 119 | } 120 | 121 | public static function removeDefaultParameters($type, $parameters) 122 | { 123 | $cleaned = []; 124 | $defaults = static::getParameterDefaults($type); 125 | 126 | foreach ($defaults as $key => $value) { 127 | if (array_key_exists($key, $parameters)) { 128 | $value = $parameters[$key]; 129 | unset($parameters[$key]); 130 | } 131 | 132 | $parameters[$key] = $value; 133 | } 134 | 135 | $canBePositional = true; 136 | 137 | foreach ($parameters as $key => $value) { 138 | if (array_key_exists($key, $defaults) && $defaults[$key] === $value) { 139 | $canBePositional = false; 140 | continue; 141 | } 142 | 143 | if ($canBePositional) { 144 | $cleaned[] = $value; 145 | } else { 146 | $cleaned[$key] = $value; 147 | } 148 | } 149 | 150 | return $cleaned; 151 | } 152 | 153 | protected function exportUp(): string 154 | { 155 | $indexModifiers = $this->buildIndexModifiers(); 156 | 157 | if (null !== $this->collapsedType && empty($this->collapsedAttributes)) { 158 | $type = $this->collapsedType; 159 | $modifiers = array_merge($this->extractModifiers($this->collapsedAttributes), $indexModifiers); 160 | 161 | $parameters = in_array($type, ['id', 'rememberToken', 'softDeletes']) ? [] : [$this->definition->name]; 162 | if ('id' === $type && 'id' !== $this->definition->name) { 163 | $parameters[] = $this->definition->name; 164 | } 165 | 166 | if ($this->foreignKey && 'unsignedBigInteger' === $type) { 167 | $modifiers = array_merge([static::makeModifier('constrained', array_filter([ 168 | $this->foreignKey->on, 169 | 'id' == $this->foreignKey->references ? null : $this->foreignKey->references, 170 | $this->foreignKey->index 171 | ]))], $modifiers); 172 | return $this->exportMethodCall('foreignId', $parameters, $modifiers); 173 | } 174 | 175 | return $this->exportMethodCall($type, $parameters, $modifiers); 176 | } else { 177 | $parameters = Column::getParameters($this->definition->type, $this->attributes); 178 | $modifiers = array_merge($this->extractModifiers($this->attributes), $indexModifiers); 179 | 180 | if (empty(array_diff(array_keys($this->attributes), array_keys($parameters)))) { 181 | array_unshift($parameters, $this->definition->name); 182 | $parameters = static::removeDefaultParameters($this->definition->type, $parameters); 183 | return $this->exportMethodCall($this->definition->type, $parameters, $modifiers); 184 | } 185 | 186 | return $this->exportMethodCall('addColumn', [ 187 | $this->definition->type, 188 | $this->definition->name, 189 | $this->attributes, 190 | ], $modifiers); 191 | } 192 | } 193 | 194 | protected function exportDown(): string 195 | { 196 | return $this->exportMethodCall('dropColumn', [$this->definition->name]); 197 | } 198 | 199 | protected function runCollapsables() 200 | { 201 | $this->collapsedAttributes = $this->attributes; 202 | 203 | $this->collapseUnsigned(); 204 | $this->collapseSoftDeletes(); 205 | $this->collapseRememberToken(); 206 | } 207 | 208 | protected function collapseId() 209 | { 210 | if ('bigIncrements' !== $this->collapsedType) { 211 | return; 212 | } 213 | 214 | $this->collapsedType = 'id'; 215 | } 216 | 217 | protected function collapseIncrements() 218 | { 219 | if ( 220 | !in_array($this->collapsedType, [ 221 | 'unsignedTinyInteger', 222 | 'unsignedSmallInteger', 223 | 'unsignedMediumInteger', 224 | 'unsignedInteger', 225 | 'unsignedBigInteger' 226 | ]) 227 | ) { 228 | return; 229 | } 230 | 231 | if (true === ($this->collapsedAttributes['autoIncrement'] ?? null)) { 232 | $this->collapsedType = match ($this->collapsedType) { 233 | 'unsignedTinyInteger' => 'tinyIncrements', 234 | 'unsignedSmallInteger' => 'smallIncrements', 235 | 'unsignedMediumInteger' => 'mediumIncrements', 236 | 'unsignedInteger' => 'increments', 237 | 'unsignedBigInteger' => 'bigIncrements', 238 | }; 239 | unset($this->collapsedAttributes['autoIncrement']); 240 | } 241 | 242 | $this->collapseId(); 243 | } 244 | 245 | protected function collapseUnsigned() 246 | { 247 | if ( 248 | !in_array($this->definition->type, [ 249 | 'tinyInteger', 250 | 'smallInteger', 251 | 'mediumInteger', 252 | 'integer', 253 | 'bigInteger' 254 | ]) 255 | ) { 256 | return; 257 | } 258 | 259 | if (true === ($this->collapsedAttributes['unsigned'] ?? null)) { 260 | $this->collapsedType = match ($this->definition->type) { 261 | 'tinyInteger' => 'unsignedTinyInteger', 262 | 'smallInteger' => 'unsignedSmallInteger', 263 | 'mediumInteger' => 'unsignedMediumInteger', 264 | 'integer' => 'unsignedInteger', 265 | 'bigInteger' => 'unsignedBigInteger', 266 | }; 267 | unset($this->collapsedAttributes['unsigned']); 268 | } 269 | 270 | $this->collapseIncrements($this->collapsedAttributes); 271 | } 272 | 273 | protected function collapseSoftDeletes() 274 | { 275 | if ( 276 | !in_array($this->definition->type, [ 277 | 'timestamp', 278 | 'timestampTz', 279 | ]) 280 | ) { 281 | return; 282 | } 283 | 284 | if (true === ($this->collapsedAttributes['nullable'] ?? null) && 'deleted_at' === $this->definition->name) { 285 | $this->collapsedType = match ($this->definition->type) { 286 | 'timestamp' => 'softDeletes', 287 | 'timestampTz' => 'softDeletesTz', 288 | }; 289 | unset($this->collapsedAttributes['nullable']); 290 | } 291 | } 292 | 293 | protected function collapseRememberToken() 294 | { 295 | if ('string' !== $this->definition->type) { 296 | return; 297 | } 298 | 299 | if ( 300 | 100 === ($this->collapsedAttributes['length'] ?? null) && 301 | 'remember_token' === $this->definition->name && 302 | $this->definition->nullable 303 | ) { 304 | $this->collapsedType = 'rememberToken'; 305 | unset($this->collapsedAttributes['length']); 306 | } 307 | } 308 | 309 | public function getCollapsedType() 310 | { 311 | return $this->collapsedType; 312 | } 313 | } 314 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/Exporter.php: -------------------------------------------------------------------------------- 1 | $value) { 30 | $valueRepresentation = static::varExport($value); 31 | if ($i === $key) { 32 | $i++; 33 | $components[] = $valueRepresentation; 34 | continue; 35 | } 36 | 37 | $components[] = static::varExport($key) . ' => ' . $valueRepresentation; 38 | } 39 | 40 | $totalLength = array_sum(array_map('strlen', $components)); 41 | 42 | if ($totalLength > static::WRAP_LIMIT) { 43 | $start = "\n\t"; 44 | $end = ",\n"; 45 | $separator = ",\n\t"; 46 | } else { 47 | $start = ''; 48 | $end = ''; 49 | $separator = ', '; 50 | } 51 | 52 | return "[{$start}" . implode($separator, $components) . "{$end}]"; 53 | } elseif (is_null($variable)) { 54 | return 'null'; 55 | } 56 | 57 | return (string) var_export($variable, true); 58 | } 59 | 60 | public function export(int $mode = self::MODE_UP): string 61 | { 62 | switch ($mode) { 63 | case static::MODE_UP: 64 | return $this->exportUp(); 65 | case static::MODE_DOWN: 66 | // no break 67 | default: 68 | return $this->exportDown(); 69 | } 70 | } 71 | 72 | public static function exportDefinition( 73 | Blueprint|Fluent|BlueprintDiff $definition, 74 | int $mode = self::MODE_UP 75 | ): string { 76 | return App::make(static::class, ['definition' => $definition])->export($mode); 77 | } 78 | 79 | /** 80 | * @param array $exports 81 | * @return string 82 | */ 83 | protected static function joinExports(array $exports): string 84 | { 85 | $items = []; 86 | 87 | foreach ($exports as $export) { 88 | $items[] = is_array($export) ? static::joinExports($export) : $export; 89 | } 90 | 91 | $filteredItems = array_values(array_filter($items, function ($item) { 92 | return null === $item ? true : !empty($item); 93 | })); 94 | 95 | while ( 96 | !empty($filteredItems) && 97 | (null === $filteredItems[0] || !trim($filteredItems[0])) 98 | ) { 99 | array_shift($filteredItems); 100 | } 101 | 102 | while ( 103 | !empty($filteredItems) && 104 | (null === last($filteredItems) || !trim(last($filteredItems))) 105 | ) { 106 | array_pop($filteredItems); 107 | } 108 | 109 | return implode("\n", $filteredItems); 110 | } 111 | 112 | protected static function exportParameters(array $parameters): string 113 | { 114 | $positionals = []; 115 | $nameds = []; 116 | 117 | foreach ($parameters as $key => $value) { 118 | if (is_int($key)) { 119 | $positionals[] = static::varExport($value, true); 120 | } else { 121 | $nameds[] = "{$key}: " . static::varExport($value, true); 122 | } 123 | } 124 | 125 | $items = array_merge($positionals, $nameds); 126 | 127 | $totalLength = array_sum(array_map('strlen', $items)); 128 | 129 | if ($totalLength > static::WRAP_LIMIT) { 130 | $parameters = str_replace("\n", "\n\t", implode(",\n", $items)); 131 | } else { 132 | $parameters = implode(', ', $items); 133 | } 134 | 135 | if (strpos($parameters, "\n") === false) { 136 | $start = ''; 137 | $end = ''; 138 | } else { 139 | $start = "\n\t"; 140 | $end = "\n"; 141 | } 142 | 143 | return "{$start}" . $parameters . "{$end}"; 144 | } 145 | 146 | protected static function exportMethodCall(string $methodName, array $parameters = [], array $modifiers = []) 147 | { 148 | sort($modifiers); 149 | $joinedModifiers = static::joinModifiers($modifiers); 150 | $parameters = static::exportParameters($parameters); 151 | $unmodified = "\$table->{$methodName}({$parameters})"; 152 | 153 | if (strlen($unmodified) + strpos("{$joinedModifiers}\n", "\n") > static::WRAP_LIMIT) { 154 | $unmodified .= "\n\t"; 155 | } 156 | 157 | return "{$unmodified}{$joinedModifiers};"; 158 | } 159 | 160 | protected static function makeModifier($name, $parameters) 161 | { 162 | $parameterString = static::exportParameters($parameters); 163 | return "->{$name}({$parameterString})"; 164 | } 165 | 166 | protected static function extractModifiers(&$attributes) 167 | { 168 | $modifiers = []; 169 | 170 | foreach ($attributes as $attributeName => $value) { 171 | if (!in_array($attributeName, static::SUPPORTED_MODIFIERS) || in_array($value, [null, false])) { 172 | continue; 173 | } 174 | 175 | $modifiers[] = static::makeModifier($attributeName, true === $value ? [] : [$value]); 176 | 177 | unset($attributes[$attributeName]); 178 | } 179 | 180 | return $modifiers; 181 | } 182 | 183 | protected static function joinModifiers($modifiers) 184 | { 185 | $concatenated = implode('', $modifiers); 186 | 187 | if (strlen($concatenated) > 90) { 188 | return str_replace('->', "\n\t->", $concatenated); 189 | } 190 | 191 | return $concatenated; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/IndexExporter.php: -------------------------------------------------------------------------------- 1 | definition->name; 30 | 31 | $parameters = [ 32 | count($this->definition->columns) > 1 ? $this->definition->columns : $this->definition->columns[0], 33 | ]; 34 | 35 | if (null !== $this->definition->index) { 36 | $parameters[] = $this->definition->index; 37 | } 38 | 39 | if (!in_array($method, ['spatialIndex', 'foreign']) && null !== $this->definition->algorithm) { 40 | if (null === $this->definition->index) { 41 | $parameters[] = null; 42 | } 43 | $parameters[] = $this->definition->algorithm; 44 | } 45 | 46 | $attributes = $this->definition->getAttributes(); 47 | $modifiers = static::extractModifiers($attributes); 48 | 49 | return $this->exportMethodCall($method, $parameters, $modifiers); 50 | } 51 | 52 | protected function exportDown(): string 53 | { 54 | return $this->exportMethodCall('drop' . ucfirst($this->definition->name), [$this->definition->index]); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/SortsColumns.php: -------------------------------------------------------------------------------- 1 | export(); 15 | 16 | if ($exporter->definition->primary) { 17 | $primary = $columnExport; 18 | } elseif ($exporter->foreignKey) { 19 | array_unshift($exports, $columnExport); 20 | } elseif ('id' === $exporter->getCollapsedType()) { 21 | if ('id' == $exporter->definition->name) { 22 | array_unshift($ids, $columnExport); 23 | } else { 24 | $ids[] = $columnExport; 25 | } 26 | } else { 27 | $exports[] = $columnExport; 28 | } 29 | } 30 | 31 | if ($primary) { 32 | array_unshift($ids, $primary); 33 | } 34 | 35 | array_unshift($exports, ...$ids); 36 | 37 | return $exports; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/TableDiffExporter.php: -------------------------------------------------------------------------------- 1 | joinExports([ 22 | $this->exportRename(), 23 | null, 24 | $this->exportDroppedColumns(), 25 | $this->exportModifiedColumns(), 26 | $this->exportAddedColumns(), 27 | null, 28 | $this->exportDroppedIndexes(), 29 | $this->exportRenamedIndexes(), 30 | $this->exportAddedIndexes(), 31 | null, 32 | $this->exportOptions(), 33 | ]); 34 | } 35 | 36 | protected function exportDown(): string 37 | { 38 | return $this->joinExports([ 39 | $this->exportRename(true), 40 | null, 41 | $this->exportDroppedColumns(true), 42 | $this->exportModifiedColumns(true), 43 | $this->exportAddedColumns(true), 44 | null, 45 | $this->exportDroppedIndexes(true), 46 | $this->exportRenamedIndexes(true), 47 | $this->exportAddedIndexes(true), 48 | null, 49 | $this->exportOptions(true), 50 | ]); 51 | } 52 | 53 | protected function exportRename(bool $reverse = false): string 54 | { 55 | $rename = $this->definition->getRename($reverse); 56 | 57 | if (null === $rename) { 58 | return ''; 59 | } 60 | 61 | [$oldName, $newName] = $rename; 62 | 63 | return $this->exportMethodCall('rename', [$oldName, $newName]); 64 | } 65 | 66 | /** 67 | * @param array $columns 68 | * @return string 69 | */ 70 | protected function exportAddedColumns(bool $reverse = false): string 71 | { 72 | $exporters = []; 73 | 74 | foreach ($this->definition->getAddedColumns($reverse) as $column) { 75 | /** @var ColumnExporter */ 76 | $exporter = App::make(ColumnExporter::class, ['definition' => $column]); 77 | 78 | foreach ($this->definition->getAddedIndexes($reverse) as $i => $index) { 79 | if ( 80 | IndexType::Foreign->value === $index->name && 81 | count($index->columns) === 1 && 82 | $column->name === $index->columns[0] 83 | ) { 84 | if ($exporter->setForeignKey($index)) { 85 | $indexName = $index->index ?? $this->definition->defaultIndexName($index, $reverse); 86 | $this->definition->dropAddedIndex($indexName, $reverse); 87 | break; 88 | } 89 | } 90 | } 91 | 92 | $exporters[] = $exporter; 93 | } 94 | 95 | return $this->joinExports($this->getSortedExports($exporters)); 96 | } 97 | 98 | /** 99 | * @param array $columns 100 | * @return string 101 | */ 102 | protected function exportDroppedColumns(bool $reverse = false): string 103 | { 104 | $exports = []; 105 | 106 | foreach ($this->definition->getDroppedColumns($reverse) as $column) { 107 | $exports[] = ColumnExporter::exportDefinition($column, ColumnExporter::MODE_DOWN); 108 | } 109 | 110 | return $this->joinExports($exports); 111 | } 112 | 113 | /** 114 | * @param array $columns 115 | * @return string 116 | */ 117 | protected function exportModifiedColumns(bool $reverse = false): string 118 | { 119 | $exports = []; 120 | 121 | foreach ($this->definition->getModifiedColumns($reverse) as $column) { 122 | $exports[] = ColumnDiffExporter::exportDefinition($column); 123 | } 124 | 125 | return $this->joinExports($exports); 126 | } 127 | 128 | /** 129 | * @param array $indexes 130 | * @return string 131 | */ 132 | protected function exportDroppedIndexes(bool $reverse = false): string 133 | { 134 | $exports = []; 135 | 136 | foreach ($this->definition->getDroppedIndexes($reverse) as $index) { 137 | $tmp = clone $index; 138 | $tmp->index = ($reverse ? $this->definition->from : $this->definition->to)->defaultIndexName($tmp); 139 | $exports[] = IndexExporter::exportDefinition($tmp, IndexExporter::MODE_DOWN); 140 | } 141 | 142 | return $this->joinExports($exports); 143 | } 144 | 145 | /** 146 | * @param array $indexes 147 | * @return string 148 | */ 149 | protected function exportAddedIndexes(bool $reverse = false): string 150 | { 151 | $exports = []; 152 | 153 | foreach ($this->definition->getAddedIndexes($reverse) as $index) { 154 | $exports[] = IndexExporter::exportDefinition($index); 155 | } 156 | 157 | return $this->joinExports($exports); 158 | } 159 | 160 | protected function exportRenamedIndexes(bool $reverse = false): string 161 | { 162 | $exports = []; 163 | 164 | foreach ($this->definition->getRenamedIndexes($reverse) as $from => $to) { 165 | $exports[] = IndexExporter::renameIndex($from, $to); 166 | } 167 | 168 | return $this->joinExports($exports); 169 | } 170 | 171 | protected function exportOptions(bool $reverse = false): string 172 | { 173 | $exports = []; 174 | 175 | foreach (['engine', 'charset', 'collation'] as $optionName) { 176 | $optionChange = $this->definition->getOptionChange($optionName, $reverse); 177 | if (null === $optionChange) { 178 | continue; 179 | } 180 | 181 | $exports[] = $this->exportMethodCall($optionName, [$optionChange]); 182 | } 183 | 184 | return $this->joinExports($exports); 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/Blueprint/Exporters/TableExporter.php: -------------------------------------------------------------------------------- 1 | joinExports([ 21 | $this->exportColumns(), 22 | null, 23 | $this->exportIndexes(), 24 | null, 25 | $this->exportTableOptions(), 26 | ]); 27 | } 28 | 29 | protected function exportDown(): string 30 | { 31 | return $this->exportMethodCall('drop'); 32 | } 33 | 34 | protected function exportColumns(): string 35 | { 36 | $columnExports = []; 37 | $hasTimestamps = false; 38 | 39 | $precisions = [ 40 | 'createdAt' => null, 41 | 'updatedAt' => null, 42 | ]; 43 | 44 | foreach ($this->definition->getColumns() as $column) { 45 | if ('timestamp' !== $column->type || !$column->nullable) { 46 | continue; 47 | } 48 | 49 | if ('created_at' === $column->name) { 50 | $precisions['createdAt'] = $column->precision; 51 | } elseif ('updated_at' === $column->name) { 52 | $precisions['updatedAt'] = $column->precision; 53 | } 54 | 55 | if (null !== $precisions['createdAt'] && $precisions['createdAt'] === $precisions['updatedAt']) { 56 | $hasTimestamps = true; 57 | $precision = $precisions['createdAt']; 58 | break; 59 | } 60 | } 61 | 62 | $this->definition->applyColumnIndexes(); 63 | $this->definition->stripDefaultIndexNames(); 64 | 65 | $exporters = []; 66 | $softDeletes = null; 67 | 68 | foreach ($this->definition->getColumns() as $column) { 69 | if ($hasTimestamps && in_array($column->name, ['created_at', 'updated_at'])) { 70 | continue; 71 | } 72 | 73 | /** @var ColumnExporter */ 74 | $exporter = App::make(ColumnExporter::class, ['definition' => $column]); 75 | 76 | foreach ($this->definition->getCommands() as $command) { 77 | if ( 78 | IndexType::Foreign->value === $command->name && 79 | count($command->columns) === 1 && 80 | $column->name === $command->columns[0] 81 | ) { 82 | if ($exporter->setForeignKey($command)) { 83 | $indexName = $command->index ?? $this->definition->defaultIndexName($command); 84 | $this->definition->dropForeign($indexName); 85 | break; 86 | } 87 | } 88 | } 89 | 90 | if ('softDeletes' === $exporter->getCollapsedType()) { 91 | $softDeletes = $exporter->export(); 92 | continue; 93 | } 94 | 95 | $exporters[] = $exporter; 96 | } 97 | 98 | $columnExports = $this->getSortedExports($exporters); 99 | 100 | if ($hasTimestamps) { 101 | $columnExports[] = static::exportMethodCall('timestamps', $precision ? [$precision] : []); 102 | } 103 | 104 | if (null !== $softDeletes) { 105 | $columnExports[] = $softDeletes; 106 | } 107 | 108 | return $this->joinExports($columnExports); 109 | } 110 | 111 | protected function exportIndexes(): string 112 | { 113 | $this->definition->stripDefaultIndexNames(); 114 | 115 | $indexExports = []; 116 | 117 | foreach ($this->definition->getCommands() as $command) { 118 | $type = IndexType::tryFrom(strtolower($command->name)); 119 | 120 | if (null === $type) { 121 | continue; 122 | } 123 | 124 | $indexFluent = clone $command; 125 | $indexFluent->name = $type->value; 126 | 127 | $indexExport = IndexExporter::exportDefinition($indexFluent); 128 | 129 | if (IndexType::Primary === $type) { 130 | array_unshift($indexExports, $indexExport); 131 | } else { 132 | $indexExports[] = $indexExport; 133 | } 134 | } 135 | 136 | return $this->joinExports($indexExports); 137 | } 138 | 139 | protected function exportTableOptions() 140 | { 141 | $optionExports = []; 142 | 143 | foreach (['engine', 'charset', 'collation'] as $option) { 144 | if (null === $this->definition->$option) { 145 | continue; 146 | } 147 | 148 | $optionExports[] = $this->exportMethodCall($option, [$this->definition->$option]); 149 | } 150 | 151 | return $this->joinExports($optionExports); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Blueprint/Manager.php: -------------------------------------------------------------------------------- 1 | */ 37 | protected array $relationshipMap = []; 38 | 39 | public function __construct( 40 | /** @var array */ 41 | protected array $blueprints 42 | ) { 43 | } 44 | 45 | public static function makeBlueprint(string $tableName, $prefix = ''): SimplifyingBlueprint 46 | { 47 | /** @var SimplifyingBlueprint */ 48 | $blueprint = App::make(SimplifyingBlueprint::class, ['tableName' => $prefix . $tableName]); 49 | return $blueprint; 50 | } 51 | 52 | /** @return array */ 53 | public function getBlueprints(): array 54 | { 55 | return $this->blueprints; 56 | } 57 | 58 | /** @return array */ 59 | public function getRelationshipMap(): array 60 | { 61 | return $this->relationshipMap; 62 | } 63 | 64 | public static function getImplications( 65 | ReflectionClass|ReflectionMethod|ReflectionProperty $reflection, 66 | string $implicationType = MigrationAttribute::class 67 | ): array { 68 | $attributeReflections = $reflection->getAttributes($implicationType, ReflectionAttribute::IS_INSTANCEOF); 69 | $attributes = array_map(fn (ReflectionAttribute $item) => $item->newInstance(), $attributeReflections); 70 | 71 | foreach (explode("\n", $reflection->getDocComment()) as $docLine) { 72 | if (!preg_match('/^\s*\/?\*\*?\s*@([a-z]+)(?=\((.*)\))?/i', $docLine, $matches)) { 73 | continue; 74 | } 75 | 76 | $className = '\\Toramanlis\\ImplicitMigrations\\Attributes\\' . Str::ucfirst($matches[1]); 77 | 78 | if (!class_exists($className) || !is_a($className, $implicationType, true)) { 79 | continue; 80 | } 81 | 82 | $parameters = []; 83 | $positionalAllowed = true; 84 | 85 | foreach (explode(',', $matches[2] ?? '') as $segment) { 86 | $segment = trim($segment); 87 | 88 | if (preg_match('/([a-z0-9]+)\s*:\s*(.*)/i', $segment, $submatches)) { 89 | $parameters[trim($submatches[1])] = eval("return {$submatches[2]};"); 90 | $positionalAllowed = false; 91 | } elseif ($positionalAllowed) { 92 | $parameters[] = eval("return {$segment};"); 93 | } 94 | } 95 | 96 | $attributes[] = new $className(...$parameters); 97 | } 98 | 99 | return array_filter($attributes, fn (MigrationAttribute $attribute) => $attribute->enabled()); 100 | } 101 | 102 | protected static function getMigrationAttributes(string $modelName): array 103 | { 104 | $reflection = new ReflectionClass($modelName); 105 | 106 | $attributes = []; 107 | 108 | $implications = static::getImplications($reflection); 109 | foreach ($implications as $attribute) { 110 | $attribute->inferFromReflectionClass($reflection); 111 | $attribute->inferFromExistingData(); 112 | 113 | $attributes[] = $attribute; 114 | } 115 | 116 | foreach ($reflection->getProperties() as $propertyReflection) { 117 | $implications = static::getImplications($propertyReflection); 118 | 119 | if ( 120 | 0 === count($implications) 121 | && !static::isPropertyOff($modelName, $propertyReflection->getName()) 122 | ) { 123 | $attribute = App::make(Column::class); 124 | $attribute->setInferred(); 125 | $attribute->inferFromReflectionProperty($propertyReflection); 126 | $attribute->inferFromExistingData(); 127 | $attributes[] = $attribute; 128 | } 129 | 130 | foreach ($implications as $attribute) { 131 | $attribute->inferFromReflectionProperty($propertyReflection); 132 | $attribute->inferFromExistingData(); 133 | 134 | $attributes[] = $attribute; 135 | } 136 | } 137 | 138 | return $attributes; 139 | } 140 | 141 | /** 142 | * @param array $migrations 143 | * @return array 144 | */ 145 | public static function mergeMigrationsToBlueprints(array $migrations): array 146 | { 147 | $blueprints = []; 148 | foreach ($migrations as $migration) { 149 | $blueprints[$migration->getSource()] = $blueprints[$migration->getSource()] 150 | ?? App::make(SimplifyingBlueprint::class, ['tableName' => $migration::TABLE_NAME]); 151 | 152 | $blueprint = $blueprints[$migration->getSource()]; 153 | $migration->tableUp($blueprint); 154 | $blueprint->separateIndexesFromColumns(); 155 | } 156 | 157 | return $blueprints; 158 | } 159 | 160 | /** 161 | * @param string $modelName 162 | * @return array 163 | */ 164 | public static function getRelationships(string $modelName): array 165 | { 166 | $modelReflection = new ReflectionClass($modelName); 167 | $modelInstance = new $modelName(); 168 | 169 | $relationships = []; 170 | 171 | foreach ($modelReflection->getMethods(ReflectionMethod::IS_PUBLIC) as $methodReflection) { 172 | if ( 173 | $methodReflection->isAbstract() 174 | || $methodReflection->isStatic() 175 | || $methodReflection->getNumberOfRequiredParameters() 176 | ) { 177 | continue; 178 | } 179 | 180 | if ( 181 | !count(static::getImplications($methodReflection, Relationship::class)) && 182 | !count(static::getImplications($methodReflection, PivotColumn::class)) && 183 | ( 184 | static::isMethodOff($modelName, $methodReflection->getShortName()) || 185 | !$methodReflection->hasReturnType() || 186 | !is_a((string) $methodReflection->getReturnType(), Relation::class, true) 187 | ) 188 | ) { 189 | continue; 190 | } 191 | 192 | $methodName = $methodReflection->getShortName(); 193 | $methodRelationships = RelationshipResolver::resolve($modelInstance->$methodName()); 194 | 195 | foreach ($methodRelationships as $relationship) { 196 | $relationship->setSource("{$modelName}::{$methodName}"); 197 | } 198 | 199 | $relationships = array_merge($relationships, $methodRelationships); 200 | 201 | if ( 202 | count($methodRelationships) !== 1 203 | || !$methodRelationships[0] instanceof IndirectRelationship 204 | ) { 205 | continue; 206 | } 207 | 208 | /** @var IndirectRelationship */ 209 | $relationship = $methodRelationships[0]; 210 | 211 | $pivotColumnAttributes = static::getImplications($methodReflection, PivotColumn::class); 212 | $pivotTableAttribute = static::getImplications($methodReflection, PivotTable::class)[0] ?? 213 | new PivotTable(); 214 | 215 | $relationship->setPivotColumnAttributes($pivotColumnAttributes); 216 | $relationship->setPivotTableAttribute($pivotTableAttribute); 217 | } 218 | 219 | return $relationships; 220 | } 221 | 222 | protected function getBlueprintByTable(string $table): Blueprint 223 | { 224 | if (!isset($this->blueprints[$table])) { 225 | $blueprint = static::makeBlueprint($table); 226 | $this->blueprints[$table] = $blueprint; 227 | } 228 | 229 | return $this->blueprints[$table]; 230 | } 231 | 232 | protected static function ensureKeyColumn(Blueprint $blueprint, string $columnName, string $type = 'id') 233 | { 234 | foreach ($blueprint->getColumns() as $column) { 235 | if ($column->name === $columnName) { 236 | return; 237 | } 238 | } 239 | 240 | $blueprint->$type($columnName); 241 | } 242 | 243 | protected function defineForeignKey( 244 | string $relatedTable, 245 | string $foreignKey, 246 | string $parentTable, 247 | string $localKey, 248 | string $foreignKeyAlias 249 | ) { 250 | $blueprint = $this->getBlueprintByTable($relatedTable); 251 | 252 | foreach ($blueprint->getCommands() as $command) { 253 | if ('foreign' !== $command->name) { 254 | continue; 255 | } 256 | 257 | if ( 258 | $command->columns[0] === $foreignKey && 259 | $command->references === $localKey && 260 | $command->on === $parentTable 261 | ) { 262 | return; 263 | } 264 | } 265 | 266 | $parentBlueprint = $this->getBlueprintByTable($parentTable); 267 | 268 | static::ensureKeyColumn($parentBlueprint, $localKey); 269 | 270 | $index = $blueprint->foreign($foreignKey) 271 | ->references($localKey) 272 | ->on($parentTable); 273 | 274 | $index->index = str_replace($foreignKey, $foreignKeyAlias, $index->index); 275 | 276 | return $index; 277 | } 278 | 279 | protected function applyDirectRelationshipToBlueprints(DirectRelationship $relationship) 280 | { 281 | $blueprint = $this->getBlueprintByTable($relationship->getRelatedTable()); 282 | $this->relationshipMap[$relationship->getRelatedTable()] = $relationship; 283 | 284 | static::ensureKeyColumn($blueprint, $relationship->getForeignKey(), 'unsignedBigInteger'); 285 | 286 | $this->relationshipMap[$relationship->getRelatedTable()] = $relationship; 287 | $this->relationshipMap[$relationship->getParentTable()] = $relationship; 288 | 289 | if (in_array(Polymorphic::class, class_uses_recursive($relationship))) { 290 | /** @var Polymorphic $relationship */ 291 | $this->ensureKeyColumn($blueprint, $relationship->getTypeKey(), 'string'); 292 | return; 293 | } 294 | 295 | $this->defineForeignKey( 296 | $relationship->getRelatedTable(), 297 | $relationship->getForeignKey(), 298 | $relationship->getParentTable(), 299 | $relationship->getLocalKey(), 300 | $relationship->getForeignKeyAlias() 301 | ); 302 | } 303 | 304 | protected function applyIndirectRelationshipToBlueprints(IndirectRelationship $relationship) 305 | { 306 | $targetBlueprint = $this 307 | ->getBlueprintByTable($relationship->getRelatedTables()[0]); 308 | $blueprint = $this 309 | ->getBlueprintByTable($relationship->getRelatedTables()[1]); 310 | $pivotBlueprint = $this 311 | ->getBlueprintByTable($relationship->getPivotTable()); 312 | 313 | $relationship->pivotTableAttribute->apply($pivotBlueprint); 314 | 315 | [ 316 | $targetBlueprint->getTable() => $targetForeignKey, 317 | $blueprint->getTable() => $foreignKey 318 | ] = $relationship->getForeignKeys(); 319 | [ 320 | $targetBlueprint->getTable() => $targetForeignKeyAlias, 321 | $blueprint->getTable() => $foreignKeyAlias 322 | ] = $relationship->getForeignKeyAliases(); 323 | [ 324 | $targetBlueprint->getTable() => $targetLocalKey, 325 | $blueprint->getTable() => $localKey 326 | ] = $relationship->getLocalKeys(); 327 | 328 | $this->relationshipMap[$blueprint->getTable()] = $relationship; 329 | $this->relationshipMap[$pivotBlueprint->getTable()] = $relationship; 330 | 331 | $this->ensureKeyColumn($pivotBlueprint, $foreignKey, 'unsignedBigInteger'); 332 | $this->ensureKeyColumn($pivotBlueprint, $targetForeignKey, 'unsignedBigInteger'); 333 | 334 | foreach ($relationship->pivotColumnAttributes as $attribute) { 335 | foreach ($pivotBlueprint->getColumns() as $column) { 336 | if ($attribute->getName() === $column->name) { 337 | continue 2; 338 | } 339 | } 340 | 341 | $attribute->apply($pivotBlueprint); 342 | } 343 | 344 | $this->relationshipMap[$targetBlueprint->getTable()] = $relationship; 345 | $this->ensureKeyColumn($targetBlueprint, $targetLocalKey); 346 | 347 | $this->defineForeignKey( 348 | $relationship->getPivotTable(), 349 | $targetForeignKey, 350 | $targetBlueprint->getTable(), 351 | $targetLocalKey, 352 | $targetForeignKeyAlias 353 | ); 354 | 355 | if (in_array(Polymorphic::class, class_uses_recursive($relationship))) { 356 | /** @var Polymorphic $relationship */ 357 | $this->ensureKeyColumn($pivotBlueprint, $relationship->getTypeKey(), 'string'); 358 | return; 359 | } 360 | 361 | $this->defineForeignKey( 362 | $relationship->getPivotTable(), 363 | $foreignKey, 364 | $blueprint->getTable(), 365 | $localKey, 366 | $foreignKeyAlias 367 | ); 368 | } 369 | 370 | protected function applyRelationshipToBlueprints(RelationshipsRelationship $relationship) 371 | { 372 | if (!$relationship->isReady()) { 373 | return; 374 | } 375 | 376 | if ($relationship instanceof DirectRelationship) { 377 | $this->applyDirectRelationshipToBlueprints($relationship); 378 | } elseif ($relationship instanceof IndirectRelationship) { 379 | $this->applyIndirectRelationshipToBlueprints($relationship); 380 | } 381 | } 382 | 383 | public function applyRelationshipsToBlueprints(array $relationships) 384 | { 385 | foreach ($relationships as $relationship) { 386 | $this->applyRelationshipToBlueprints($relationship); 387 | } 388 | } 389 | 390 | protected static function isModelOff(string $modelName): bool 391 | { 392 | $modelReflection = new ReflectionClass($modelName); 393 | $attributes = static::getImplications($modelReflection, Off::class); 394 | 395 | return !Config::get('database.auto_infer_migrations') || 0 !== count($attributes); 396 | } 397 | 398 | protected static function isPropertyOff($modelName, $propertyName): bool 399 | { 400 | if (static::isModelOff($modelName)) { 401 | return true; 402 | } 403 | 404 | if (property_exists(Model::class, $propertyName)) { 405 | return true; 406 | } 407 | 408 | $modelReflection = new ReflectionClass($modelName); 409 | $explicitlyOff = false; 410 | 411 | if ($modelReflection->hasProperty($propertyName)) { 412 | $propertyReflection = new ReflectionProperty($modelName, $propertyName); 413 | $attributes = static::getImplications($propertyReflection, Off::class); 414 | $explicitlyOff = 0 !== count($attributes); 415 | } 416 | 417 | return !Config::get('database.auto_infer_migrations') || $explicitlyOff; 418 | } 419 | 420 | protected static function isMethodOff($modelName, $methodName): bool 421 | { 422 | if (static::isModelOff($modelName)) { 423 | return true; 424 | } 425 | 426 | if (method_exists(Model::class, $methodName)) { 427 | return true; 428 | } 429 | 430 | $modelReflection = new ReflectionClass($modelName); 431 | $explicitlyOff = false; 432 | 433 | if ($modelReflection->hasMethod($methodName)) { 434 | $methodReflection = new ReflectionMethod($modelName, $methodName); 435 | $attributes = static::getImplications($methodReflection, Off::class); 436 | $explicitlyOff = 0 !== count($attributes); 437 | } 438 | 439 | return !Config::get('database.auto_infer_migrations') || $explicitlyOff; 440 | } 441 | 442 | public static function generateBlueprint(string $modelName): ?SimplifyingBlueprint 443 | { 444 | $attributes = static::getMigrationAttributes($modelName); 445 | 446 | if (empty($attributes) && static::isModelOff($modelName)) { 447 | return null; 448 | } 449 | 450 | /** @var Model */ 451 | $instance = new $modelName(); 452 | $table = static::makeBlueprint($tableAttribute->name ?? $instance->getTable(), $tableAttribute->prefix ?? ''); 453 | 454 | foreach ($attributes as $attribute) { 455 | /** @var Table|Column|Index|ForeignKey $attribute */ 456 | $attribute->apply($table); 457 | } 458 | 459 | static::inferPrimaryKey($modelName, $table); 460 | static::inferTimestamps($modelName, $table); 461 | static::inferSoftDeletes($modelName, $table); 462 | 463 | return $table; 464 | } 465 | 466 | public function ensureIndexColumns(array $modelNames): void 467 | { 468 | foreach ($modelNames as $modelName) { 469 | $table = $this->blueprints[(new $modelName())->getTable()] ?? null; 470 | 471 | if (!$table) { 472 | continue; 473 | } 474 | 475 | $attributes = static::getMigrationAttributes($modelName); 476 | foreach ($attributes as $attribute) { 477 | if ($attribute instanceof ForeignKey) { 478 | $attribute->ensureColumns($table, $this->blueprints, $modelNames); 479 | } elseif ($attribute instanceof Index) { 480 | $attribute->ensureColumns($table); 481 | } 482 | } 483 | } 484 | } 485 | 486 | protected static function inferPrimaryKey(string $modelName, Blueprint $table) 487 | { 488 | /** @var Model */ 489 | $instance = new $modelName(); 490 | 491 | $columnExists = array_reduce( 492 | $table->getColumns(), 493 | fn ($carry, $column) => $carry || $column->name === $instance->getKeyName(), 494 | false 495 | ); 496 | 497 | foreach ($table->getCommands() as $command) { 498 | if ('primary' !== $command->name) { 499 | continue; 500 | } 501 | 502 | if ( 503 | count($command->columns) === 1 && 504 | $instance->getKeyName() === $command->columns[0] && 505 | $columnExists 506 | ) { 507 | return; 508 | } 509 | 510 | break; 511 | } 512 | 513 | if (!$columnExists) { 514 | if ('int' === $instance->getKeyType()) { 515 | if ($instance->getIncrementing()) { 516 | $table->id($instance->getKeyName()); 517 | return; 518 | } else { 519 | $table->unsignedBigInteger($instance->getKeyName(), $instance->getIncrementing()); 520 | } 521 | } else { 522 | $method = Column::TYPE_MAP[$instance->getKeyType()] ?? null; 523 | if (null === $method) { 524 | return; 525 | } 526 | 527 | $table->$method($instance->getKeyName()); 528 | } 529 | } 530 | 531 | $table->primary($instance->getKeyName()); 532 | } 533 | 534 | protected static function inferTimestamps(string $modelName, Blueprint $table) 535 | { 536 | /** @var Model */ 537 | $instance = new $modelName(); 538 | 539 | if (!$instance->usesTimestamps()) { 540 | return; 541 | } 542 | 543 | $createdAtColumn = $instance->getCreatedAtColumn(); 544 | $updatedAtColumn = $instance->getUpdatedAtColumn(); 545 | 546 | foreach ($table->getColumns() as $column) { 547 | if ($column->name === $createdAtColumn) { 548 | $createdAtColumn = null; 549 | } 550 | 551 | if ($column->name === $updatedAtColumn) { 552 | $updatedAtColumn = null; 553 | } 554 | } 555 | 556 | if (null !== $createdAtColumn) { 557 | $table->timestamp($createdAtColumn)->nullable(); 558 | } 559 | 560 | if (null !== $updatedAtColumn) { 561 | $table->timestamp($updatedAtColumn)->nullable(); 562 | } 563 | } 564 | 565 | protected static function inferSoftDeletes(string $modelName, Blueprint $table) 566 | { 567 | if (!in_array(SoftDeletes::class, class_uses_recursive($modelName))) { 568 | return; 569 | } 570 | 571 | /** @var SoftDeletes */ 572 | $instance = new $modelName(); 573 | 574 | $deletedAtColumn = $instance->getDeletedAtColumn(); 575 | 576 | foreach ($table->getColumns() as $column) { 577 | if ($column->name === $deletedAtColumn) { 578 | return; 579 | } 580 | } 581 | 582 | $table->softDeletes($deletedAtColumn); 583 | } 584 | 585 | public static function getDiff(SimplifyingBlueprint $from, SimplifyingBlueprint $to): BlueprintDiff 586 | { 587 | /** @var BlueprintDiff */ 588 | $diff = App::make(BlueprintDiff::class, [ 589 | 'from' => $from, 590 | 'to' => $to, 591 | ]); 592 | 593 | return $diff; 594 | } 595 | } 596 | -------------------------------------------------------------------------------- /src/Blueprint/Migratable.php: -------------------------------------------------------------------------------- 1 | parentTable 21 | && null !== $this->relatedTable 22 | && null !== $this->foreignKey 23 | && null !== $this->localKey; 24 | } 25 | 26 | public function setParentTable(string $parentTable): static 27 | { 28 | $this->parentTable = $parentTable; 29 | return $this; 30 | } 31 | 32 | public function setRelatedTable(string $relatedTable): static 33 | { 34 | $this->relatedTable = $relatedTable; 35 | return $this; 36 | } 37 | 38 | public function setForeignKey(string $foreignKey): static 39 | { 40 | $this->foreignKey = $foreignKey; 41 | return $this; 42 | } 43 | 44 | public function setLocalKey(string $localKey): static 45 | { 46 | $this->localKey = $localKey; 47 | return $this; 48 | } 49 | 50 | public function getParentTable(): string 51 | { 52 | if (null === $this->parentTable) { 53 | throw new Exception(Exception::CODE_RL_NO_PARENT); 54 | } 55 | 56 | return $this->parentTable; 57 | } 58 | 59 | public function getRelatedTable(): string 60 | { 61 | if (null === $this->relatedTable) { 62 | throw new Exception(Exception::CODE_RL_NO_RELATED); 63 | } 64 | 65 | return $this->relatedTable; 66 | } 67 | 68 | public function getForeignKey(): string 69 | { 70 | if (null === $this->foreignKey) { 71 | throw new Exception(Exception::CODE_RL_NO_FOREIGN); 72 | } 73 | 74 | return $this->foreignKey; 75 | } 76 | 77 | public function getLocalKey(): string 78 | { 79 | if (null === $this->localKey) { 80 | throw new Exception(Exception::CODE_RL_NO_LOCAL); 81 | } 82 | 83 | return $this->localKey; 84 | } 85 | 86 | public function getForeignKeyAlias(): string 87 | { 88 | return $this->getForeignKey(); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/Blueprint/Relationships/IndirectRelationship.php: -------------------------------------------------------------------------------- 1 | */ 15 | public readonly array $pivotColumnAttributes; 16 | 17 | /** 18 | * @param null|string $pivotTable 19 | * @param array $relatedTables 20 | * @param array $foreignKeys 21 | * @param array $localKeys 22 | * @param array $pivotColumns 23 | */ 24 | public function __construct( 25 | protected ?string $pivotTable = null, 26 | protected array $relatedTables = [], 27 | protected array $foreignKeys = [], 28 | protected array $localKeys = [], 29 | protected array $pivotColumns = [] 30 | ) { 31 | } 32 | 33 | public function isReady(): bool 34 | { 35 | return null !== $this->pivotTable 36 | && !empty($this->relatedTables) 37 | && !empty($this->foreignKeys) 38 | && !empty($this->localKeys); 39 | } 40 | 41 | /** 42 | * @param string $pivotTable 43 | * @return static 44 | */ 45 | public function setPivotTable(string $pivotTable): static 46 | { 47 | $this->pivotTable = $pivotTable; 48 | return $this; 49 | } 50 | 51 | /** 52 | * @param array $relatedTables 53 | * @return static 54 | */ 55 | public function setRelatedTables(array $relatedTables): static 56 | { 57 | $this->relatedTables = $relatedTables; 58 | return $this; 59 | } 60 | 61 | /** 62 | * @param string $relatedTable 63 | * @return static 64 | */ 65 | public function addRelatedTable(string $relatedTable): static 66 | { 67 | $this->relatedTables[] = $relatedTable; 68 | return $this; 69 | } 70 | 71 | /** 72 | * @param array $foreignKeys 73 | * @return static 74 | */ 75 | public function setForeignKeys(array $foreignKeys): static 76 | { 77 | $this->foreignKeys = $foreignKeys; 78 | return $this; 79 | } 80 | 81 | /** 82 | * @param string $relatedTable 83 | * @param string $foreignKey 84 | * @return static 85 | */ 86 | public function addForeignKey(string $relatedTable, string $foreignKey): static 87 | { 88 | $this->foreignKeys[$relatedTable] = $foreignKey; 89 | return $this; 90 | } 91 | 92 | /** 93 | * @param array $localKeys 94 | * @return static 95 | */ 96 | public function setLocalKeys(array $localKeys): static 97 | { 98 | $this->localKeys = $localKeys; 99 | return $this; 100 | } 101 | 102 | /** 103 | * @param string $tableName 104 | * @param string $localKey 105 | * @return static 106 | */ 107 | public function addLocalKey(string $tableName, string $localKey): static 108 | { 109 | $this->localKeys[$tableName] = $localKey; 110 | return $this; 111 | } 112 | 113 | /** 114 | * @param array $pivotColumns 115 | * @return static 116 | */ 117 | public function setPivotColumns(array $pivotColumns): static 118 | { 119 | $this->pivotColumns = $pivotColumns; 120 | return $this; 121 | } 122 | 123 | /** 124 | * @param string $pivotColumn 125 | * @return static 126 | */ 127 | public function addPivotColumn(string $pivotColumn): static 128 | { 129 | $this->pivotColumns[] = $pivotColumn; 130 | return $this; 131 | } 132 | 133 | public function setPivotTableAttribute(PivotTable $attribute) 134 | { 135 | $this->pivotTableAttribute = $attribute; 136 | $attribute->name ??= $this->pivotTable; 137 | $this->pivotTable = $attribute->name; 138 | } 139 | 140 | /** 141 | * @param array $pivotColumnAttributes 142 | * @return static 143 | */ 144 | public function setPivotColumnAttributes(array $pivotColumnAttributes): static 145 | { 146 | $this->pivotColumnAttributes = $pivotColumnAttributes; 147 | return $this; 148 | } 149 | 150 | /** 151 | * @return string 152 | * @throws Exception 153 | */ 154 | public function getPivotTable(): string 155 | { 156 | if (null === $this->pivotTable) { 157 | throw new Exception(Exception::CODE_RL_NO_PIVOT); 158 | } 159 | 160 | return $this->pivotTable; 161 | } 162 | 163 | /** @return array */ 164 | public function getRelatedTables(): array 165 | { 166 | return $this->relatedTables; 167 | } 168 | 169 | /** @return array */ 170 | public function getForeignKeys(): array 171 | { 172 | return $this->foreignKeys; 173 | } 174 | 175 | /** @return array */ 176 | public function getLocalKeys(): array 177 | { 178 | return $this->localKeys; 179 | } 180 | 181 | /** 182 | * @return array 183 | */ 184 | public function getPivotColumns(): array 185 | { 186 | return $this->pivotColumns; 187 | } 188 | 189 | public function getForeignKeyAliases(): array 190 | { 191 | return $this->foreignKeys; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/Blueprint/Relationships/MorphicDirectRelationship.php: -------------------------------------------------------------------------------- 1 | getRelatedTables(), 15 | function ($carry, $table) { 16 | $carry[$table] = Str::singular($table) . '_' . $this->getLocalKeys()[$table]; 17 | return $carry; 18 | }, 19 | [] 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Blueprint/Relationships/Polymorphic.php: -------------------------------------------------------------------------------- 1 | typeKey = $typeKey; 12 | return $this; 13 | } 14 | 15 | public function getTypeKey(): string 16 | { 17 | return $this->typeKey; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/Blueprint/Relationships/Relationship.php: -------------------------------------------------------------------------------- 1 | source = $source; 14 | return $this; 15 | } 16 | 17 | public function getSource(): string 18 | { 19 | return $this->source; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Blueprint/SimplifyingBlueprint.php: -------------------------------------------------------------------------------- 1 | useDefaultSchemaGrammar(); 20 | parent::__construct($connection, $prefix . $tableName); 21 | } 22 | 23 | public function applyColumnIndexes() 24 | { 25 | $applicables = [IndexType::Primary->value, IndexType::Unique->value, IndexType::Index->value]; 26 | foreach ($this->commands as $command) { 27 | if ( 28 | !in_array($command->name, $applicables) || 29 | 1 !== count($command->columns) 30 | ) { 31 | continue; 32 | } 33 | 34 | foreach ($this->columns as $column) { 35 | if ($column->name === $command->columns[0]) { 36 | $defaultName = $this->defaultIndexName($command); 37 | $column->{$command->name} = $command->index === $defaultName ? true : $command->index; 38 | $this->dropIndex($this->indexName($command)); 39 | break; 40 | } 41 | } 42 | } 43 | } 44 | 45 | public function stripDefaultIndexNames() 46 | { 47 | foreach ($this->getCommands() as $command) { 48 | if (!IndexType::tryFrom($command->name)) { 49 | continue; 50 | } 51 | 52 | if ($this->defaultIndexName($command) === $command->index) { 53 | $command->index = null; 54 | } 55 | } 56 | } 57 | 58 | public function defaultIndexName(Fluent $index) 59 | { 60 | return $this->createIndexName($index->name, $index->columns); 61 | } 62 | 63 | public function indexName(Fluent $index): string 64 | { 65 | return $index->index ?? $this->defaultIndexName($index); 66 | } 67 | 68 | public function separateIndexesFromColumns() 69 | { 70 | foreach ($this->columns as $column) { 71 | foreach ([IndexType::Primary->value, IndexType::Unique->value, IndexType::Index->value] as $indexType) { 72 | if (!$column->$indexType) { 73 | continue; 74 | } 75 | 76 | $this->$indexType($column->name, true === $column->$indexType ? null : $column->$indexType); 77 | unset($column->$indexType); 78 | } 79 | } 80 | } 81 | 82 | public function removeDuplicatePrimaries() 83 | { 84 | foreach ($this->commands as $command) { 85 | if ( 86 | $command->name !== IndexType::Primary->value || 87 | count($command->columns) !== 1 88 | ) { 89 | continue; 90 | } 91 | 92 | foreach ($this->columns as $column) { 93 | if ($column->name !== $command->columns[0]) { 94 | continue; 95 | } 96 | 97 | if ($column->autoIncrement || $column->primary) { 98 | $this->dropIndex($this->indexName($command)); 99 | } 100 | } 101 | } 102 | } 103 | 104 | public function addColumn($type, $name, array $parameters = []) 105 | { 106 | parent::addColumn($type, $name, $parameters); 107 | 108 | foreach ($this->columns as $i => $column) { 109 | if ($column->name !== $name) { 110 | continue; 111 | } 112 | 113 | $newColumn = array_pop($this->columns); 114 | 115 | $this->columns[$i] = $newColumn; 116 | break; 117 | } 118 | 119 | return $newColumn; 120 | } 121 | 122 | public function dropColumn($columns) 123 | { 124 | $columns = is_array($columns) ? $columns : func_get_args(); 125 | 126 | $remainingColumns = []; 127 | 128 | foreach ($this->columns as $column) { 129 | if (!in_array($column->name, $columns)) { 130 | $remainingColumns[] = $column; 131 | continue; 132 | } 133 | 134 | foreach ($this->commands as $command) { 135 | if (null === IndexType::tryFrom($command->name)) { 136 | continue; 137 | } 138 | 139 | if (in_array($column->name, $command->columns)) { 140 | $this->dropIndex($this->indexName($command)); 141 | } 142 | } 143 | } 144 | 145 | $this->columns = $remainingColumns; 146 | 147 | return App::make(Fluent::class); 148 | } 149 | 150 | public function renameColumn($from, $to) 151 | { 152 | foreach ($this->columns as $column) { 153 | if ($column->name !== $from) { 154 | continue; 155 | } 156 | 157 | $column->name = $to; 158 | break; 159 | } 160 | 161 | return $column; 162 | } 163 | 164 | public function renameIndex($from, $to) 165 | { 166 | $this->separateIndexesFromColumns(); 167 | 168 | foreach ($this->commands as $command) { 169 | if (null === IndexType::tryFrom($command->name) || $this->indexName($command) !== $from) { 170 | continue; 171 | } 172 | 173 | $command->index = $to; 174 | break; 175 | } 176 | 177 | return $command; 178 | } 179 | 180 | protected function dropIndexCommand($command, $type, $index) 181 | { 182 | $remainingCommands = []; 183 | 184 | foreach ($this->commands as $command) { 185 | if ( 186 | null !== IndexType::tryFrom($command->name) && 187 | ($this->indexName($command)) === $index 188 | ) { 189 | continue; 190 | } 191 | 192 | $remainingCommands[] = $command; 193 | } 194 | 195 | $this->commands = $remainingCommands; 196 | return $command; 197 | } 198 | 199 | public function getDependedColumnNames(): array 200 | { 201 | $dependedColumnNames = []; 202 | foreach ($this->commands as $command) { 203 | if (IndexType::Foreign->value !== $command->name) { 204 | continue; 205 | } 206 | 207 | $references = is_array($command->references) ? $command->references : [$command->references]; 208 | foreach ($references as $reference) { 209 | $dependedColumnNames[] = "{$command->on}.{$reference}"; 210 | } 211 | } 212 | 213 | return $dependedColumnNames; 214 | } 215 | 216 | public function getAddedColumnNames(): array 217 | { 218 | return array_map(fn ($column) => $column->name, $this->columns); 219 | } 220 | 221 | public function extractForeignKey(string $on, string $reference): Fluent 222 | { 223 | foreach ($this->commands as $command) { 224 | $references = is_array($command->references) ? $command->references : [$command->references]; 225 | 226 | if ( 227 | IndexType::Foreign->value !== $command->name || 228 | $command->on !== $on || 229 | !in_array($reference, $references) 230 | ) { 231 | continue; 232 | } 233 | 234 | $this->dropForeign($this->indexName($command)); 235 | return $command; 236 | } 237 | 238 | throw new Exception("Reference {$on}.{$reference} has no foreign key in blueprint for {$this->getTable()}"); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/Console/Commands/GenerateMigrationCommand.php: -------------------------------------------------------------------------------- 1 | */ 29 | protected array $modelNames; 30 | 31 | /** array */ 32 | protected array $migrationPaths; 33 | 34 | protected MigrationGenerator $generator; 35 | 36 | public function handle() 37 | { 38 | $migrator = resolve('migrator'); 39 | $this->migrationPaths = array_merge($migrator->paths(), [database_path('migrations')]); 40 | $this->modelNames = $this->argument('models') ?: 41 | $this->getModelNames(Config::get('database.model_paths')); 42 | 43 | $migrations = $this->getImplicitMigrations(); 44 | 45 | /** @var MigrationGenerator */ 46 | $generator = App::make(MigrationGenerator::class, ['existingMigrations' => $migrations]); 47 | 48 | $migrationData = $generator->generate($this->modelNames); 49 | 50 | foreach ($migrationData as $tableName => $migrationItem) { 51 | $modelName = $migrationItem['modelName']; 52 | $reflection = new ReflectionClass($modelName); 53 | $modelFile = $reflection->getFileName(); 54 | 55 | $migrationPath = $this->generateMigrationFilePath($tableName, $modelFile, $migrationItem['mode']); 56 | 57 | if (file_exists($migrationPath)) { 58 | echo "\tMigration file {$migrationPath} already exists. Skipping\n"; 59 | continue; 60 | } 61 | 62 | file_put_contents($migrationPath, $migrationItem['contents']); 63 | echo "\tCreated migration: {$migrationPath}\n"; 64 | } 65 | } 66 | 67 | protected function getModelNames($modelPaths) 68 | { 69 | $modelNames = []; 70 | $modelFiles = []; 71 | 72 | foreach ($modelPaths as $modelPath) { 73 | if (!$modelPath) { 74 | continue; 75 | } 76 | 77 | $modelPath = $modelPath[0] === DIRECTORY_SEPARATOR ? $modelPath : base_path($modelPath); 78 | foreach (new FilesystemIterator($modelPath, FilesystemIterator::SKIP_DOTS) as $modelFile) { 79 | /** @var SplFileInfo $modelFile */ 80 | require_once($modelFile->getRealPath()); 81 | $modelFiles[] = $modelFile->getRealPath(); 82 | } 83 | } 84 | 85 | foreach (get_declared_classes() as $className) { 86 | if (!is_subclass_of($className, Model::class, true)) { 87 | continue; 88 | } 89 | 90 | $modelFile = (new ReflectionClass($className))->getFileName(); 91 | if (!in_array($modelFile, $modelFiles)) { 92 | continue; 93 | } 94 | 95 | $modelNames[$modelFile] = $className; 96 | } 97 | 98 | return $modelNames; 99 | } 100 | 101 | protected function getImplicitMigrations() 102 | { 103 | $implicitMigrations = []; 104 | 105 | foreach ($this->migrationPaths as $migrationPath) { 106 | $iterator = new FilesystemIterator($migrationPath, FilesystemIterator::SKIP_DOTS); 107 | foreach ($iterator as $migrationFile) { 108 | /** @var SplFileInfo $migrationFile */ 109 | $fileName = $migrationFile->getRealPath(); 110 | $migration = include($fileName); 111 | 112 | if ( 113 | !$migration instanceof Migration || 114 | !method_exists($migration, 'getSource') || 115 | count(Manager::getImplications(new ReflectionMethod($migration, 'getSource'), Off::class)) 116 | ) { 117 | continue; 118 | } 119 | 120 | $implicitMigrations[$fileName] = $migration; 121 | } 122 | } 123 | 124 | ksort($implicitMigrations, SORT_STRING); 125 | return $implicitMigrations; 126 | } 127 | 128 | protected function generateMigrationFilePath(string $tableName, string $modelFile, string $mode): string 129 | { 130 | static $nonce = 0; 131 | 132 | $fileName = date('Y_m_d_His') . 133 | '_' . 134 | $nonce++ . 135 | "_implicit_migration_{$mode}_{$tableName}_table.php"; 136 | 137 | $targetPath = null; 138 | $modelPath = $modelFile; 139 | 140 | while (null === $targetPath && $modelPath) { 141 | $modelPath = substr($modelPath, 0, (int) strrpos($modelPath, DIRECTORY_SEPARATOR)); 142 | $targetDepth = 0; 143 | 144 | foreach ($this->migrationPaths as $migrationPath) { 145 | if ( 146 | 0 !== strpos($migrationPath, $modelPath) || 147 | !(null === $targetPath || count(explode(DIRECTORY_SEPARATOR, $migrationPath)) < $targetDepth) 148 | ) { 149 | continue; 150 | } 151 | 152 | $targetPath = $migrationPath; 153 | $targetDepth = count(explode(DIRECTORY_SEPARATOR, $targetPath)); 154 | } 155 | } 156 | 157 | return $targetPath . DIRECTORY_SEPARATOR . $fileName; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/Generator/MigrationGenerator.php: -------------------------------------------------------------------------------- 1 | */ 22 | protected array $existingBlueprints = []; 23 | protected TemplateManager $createTemplateManager; 24 | protected TemplateManager $updateTemplateManager; 25 | 26 | /** 27 | * @param string $templateName 28 | * @param array $existingMigrations 29 | */ 30 | public function __construct(array $existingMigrations) 31 | { 32 | $this->existingBlueprints = Manager::mergeMigrationsToBlueprints($existingMigrations); 33 | 34 | /** @var TemplateManager */ 35 | $manager = App::make(TemplateManager::class, ['templateName' => static::CREATE_TEMPLATE]); 36 | $this->createTemplateManager = $manager; 37 | /** @var TemplateManager */ 38 | $manager = App::make(TemplateManager::class, ['templateName' => static::UPDATE_TEMPLATE]); 39 | $this->updateTemplateManager = $manager; 40 | } 41 | 42 | /** 43 | * @param array $modelNames 44 | * @return array> 45 | */ 46 | public function generate(array $modelNames): array 47 | { 48 | $migrationData = []; 49 | $blueprints = []; 50 | $relationships = []; 51 | $sourceMap = []; 52 | 53 | foreach ($modelNames as $modelName) { 54 | $modelRelationships = Manager::getRelationships($modelName); 55 | 56 | $relationships = array_merge($relationships, $modelRelationships); 57 | 58 | $blueprint = Manager::generateBlueprint($modelName); 59 | 60 | if (null === $blueprint) { 61 | continue; 62 | } 63 | 64 | $blueprints[$blueprint->getTable()] = $blueprint; 65 | $sourceMap[$blueprint->getTable()] = $modelName; 66 | } 67 | 68 | /** @var Manager */ 69 | $blueprintManager = App::make(Manager::class, ['blueprints' => $blueprints]); 70 | $blueprintManager->applyRelationshipsToBlueprints($relationships); 71 | $blueprintManager->ensureIndexColumns($modelNames); 72 | 73 | foreach ($blueprintManager->getRelationshipMap() as $tableName => $relationship) { 74 | $sourceMap[$tableName] = $sourceMap[$tableName] ?? $relationship->getSource(); 75 | } 76 | 77 | $migratables = []; 78 | foreach ($blueprintManager->getBlueprints() as $table => $blueprint) { 79 | $blueprint->removeDuplicatePrimaries(); 80 | $source = $sourceMap[$table]; 81 | if (!isset($this->existingBlueprints[$source])) { 82 | $migratables[$table] = $blueprint; 83 | continue; 84 | } 85 | 86 | $diff = Manager::getDiff($this->existingBlueprints[$source], $blueprint); 87 | 88 | if ($diff->none()) { 89 | continue; 90 | } 91 | 92 | $migratables[$table] = $diff; 93 | } 94 | 95 | $migratables = $this->sortMigrations($migratables); 96 | 97 | foreach ($migratables as $table => $migratable) { 98 | $source = $sourceMap[ltrim($table, '_')]; 99 | 100 | if ($migratable instanceof SimplifyingBlueprint) { 101 | $migrationData[$table] = $this->getMigrationItem($source, $migratable); 102 | } else { 103 | /** @var BlueprintDiff $migratable */ 104 | $migratable->applyColumnIndexes(); 105 | $migratable->applyColumnIndexes(true); 106 | $migratable->stripDefaultIndexNames(); 107 | $migratable->stripDefaultIndexNames(true); 108 | $key = isset($this->existingBlueprints[$source]) 109 | ? $this->existingBlueprints[$source]->getTable() : $table; 110 | $migrationData[$key] = $this->getMigrationItem( 111 | $source, 112 | $migratable, 113 | ); 114 | } 115 | } 116 | 117 | return $migrationData; 118 | } 119 | 120 | /** 121 | * @param array $migratables 122 | * @return array $migratables 123 | */ 124 | protected function sortMigrations(array $migratables): array 125 | { 126 | $extraMigratables = $this->separateCodependents($migratables); 127 | 128 | $sorted = []; 129 | 130 | while (count($migratables)) { 131 | $dependencyMap = $this->getDependencyMap($migratables); 132 | 133 | foreach (array_keys($migratables) as $table) { 134 | $dependencies = $dependencyMap[$table] ?? []; 135 | 136 | $counts = array_map(fn ($item) => count($item), $dependencies); 137 | if (0 !== array_sum($counts)) { 138 | continue; 139 | } 140 | 141 | $sorted[$table] = $migratables[$table]; 142 | unset($migratables[$table]); 143 | } 144 | } 145 | 146 | return array_merge($sorted, $extraMigratables); 147 | } 148 | 149 | protected function getDependencyMap($migratables) 150 | { 151 | $addedColumns = []; 152 | 153 | foreach ($migratables as $table => $migratable) { 154 | foreach ($migratable->getAddedColumnNames() as $addedColumn) { 155 | $addedColumns["{$table}.{$addedColumn}"] = $table; 156 | } 157 | } 158 | 159 | $dependencyMap = []; 160 | foreach (array_keys($migratables) as $table) { 161 | $dependencyMap[$table] = $this->getDependencies($table, $migratables, $addedColumns); 162 | } 163 | 164 | return $dependencyMap; 165 | } 166 | 167 | 168 | protected function separateCodependents($migratables): array 169 | { 170 | $extraMigratables = []; 171 | 172 | while (count($codependents = $this->getCodependents($migratables))) { 173 | $biggestDependent = null; 174 | $mostDependencies = 0; 175 | 176 | foreach ($codependents as $table => $dependencies) { 177 | foreach ($dependencies as $column => $columnDependencies) { 178 | if (count($columnDependencies) <= $mostDependencies) { 179 | continue; 180 | } 181 | 182 | $mostDependencies = count($columnDependencies); 183 | $biggestDependent = ['table' => $table, 'column' => $column]; 184 | } 185 | } 186 | 187 | [$on, $shortColumn] = explode('.', $biggestDependent['column']); 188 | $extraMigratable = App::make(BlueprintDiff::class, [ 189 | 'from' => App::make(SimplifyingBlueprint::class, ['tableName' => $biggestDependent['table']]), 190 | 'to' => App::make(SimplifyingBlueprint::class, ['tableName' => $biggestDependent['table']]), 191 | ]); 192 | $extraMigratable->addedIndexes = [ 193 | $migratables[$biggestDependent['table']]->extractForeignKey($on, $shortColumn) 194 | ]; 195 | $extraMigratables['_' . $biggestDependent['table']] = $extraMigratable; 196 | } 197 | 198 | 199 | return $extraMigratables; 200 | } 201 | 202 | protected function getCodependents($migratables) 203 | { 204 | $dependencyMap = $this->getDependencyMap($migratables); 205 | 206 | $codependents = []; 207 | foreach ($dependencyMap as $table => $dependencies) { 208 | foreach ($dependencies as $dependedColumn => $columnDependencies) { 209 | foreach ($columnDependencies as $dependency) { 210 | foreach ($dependencyMap[$dependency] as $counterColumn => $subdependencies) { 211 | if (!in_array($table, $subdependencies)) { 212 | continue; 213 | } 214 | 215 | $codependents[$table] ??= []; 216 | $codependents[$table][$dependedColumn] ??= []; 217 | if (!in_array($dependency, $codependents[$table][$dependedColumn])) { 218 | $codependents[$table][$dependedColumn][] = $dependency; 219 | } 220 | 221 | $codependents[$dependency] ??= []; 222 | $codependents[$dependency][$counterColumn] ??= []; 223 | if (!in_array($table, $codependents[$dependency][$counterColumn])) { 224 | $codependents[$dependency][$counterColumn][] = $table; 225 | } 226 | } 227 | } 228 | } 229 | } 230 | 231 | return $codependents; 232 | } 233 | 234 | protected function getDependencies($table, $migratables, $addedColumns, $chain = []): array 235 | { 236 | if (in_array($table, $chain)) { 237 | return []; 238 | } 239 | 240 | $chain = array_merge($chain, [$table]); 241 | 242 | $dependencies = []; 243 | $migratable = $migratables[$table]; 244 | foreach ($migratable->getDependedColumnNames() as $dependedColumn) { 245 | if (!isset($addedColumns[$dependedColumn])) { 246 | continue; 247 | } 248 | 249 | $dependency = $addedColumns[$dependedColumn]; 250 | $subDependencies = $this->getDependencies($dependency, $migratables, $addedColumns, $chain); 251 | $merged = array_unique(array_merge([$dependency], $subDependencies)); 252 | 253 | if (1 === count($chain)) { 254 | $dependencies[$dependedColumn] = $merged; 255 | } else { 256 | $dependencies = $merged; 257 | } 258 | } 259 | 260 | return $dependencies; 261 | } 262 | 263 | protected function getMigrationItem( 264 | string $source, 265 | Migratable $definition, 266 | ) { 267 | $modelName = explode('::', $source)[0]; 268 | $mode = $definition instanceof SimplifyingBlueprint ? MigrationMode::Create : MigrationMode::Update; 269 | 270 | $exporter = match ($mode) { 271 | MigrationMode::Create => TableExporter::class, 272 | MigrationMode::Update => TableDiffExporter::class, 273 | }; 274 | 275 | $templateManager = match ($mode) { 276 | MigrationMode::Create => $this->createTemplateManager, 277 | MigrationMode::Update => $this->updateTemplateManager, 278 | }; 279 | 280 | [$tableNameOld, $tableNameNew] = $definition instanceof Blueprint 281 | ? [$definition->getTable(), $definition->getTable()] 282 | : [$definition->from->getTable(), $definition->to->getTable()]; 283 | 284 | $tableRenamed = $tableNameNew !== $tableNameOld; 285 | [$sourceClass, $sourceMethod] = explode('::', $source . '::'); 286 | 287 | return [ 288 | 'modelName' => $modelName, 289 | 'mode' => $mode->value, 290 | 'contents' => $templateManager->process([ 291 | 'sourceClass' => $sourceClass, 292 | 'source' => $sourceMethod ? "Source::class . '::{$sourceMethod}'" : 'Source::class', 293 | 'tableNameNew' => $tableNameNew, 294 | 'tableNameOld' => $tableRenamed ? "'{$tableNameOld}'" : 'static::TABLE_NAME', 295 | 'up' => $exporter::exportDefinition($definition), 296 | 'down' => $exporter::exportDefinition($definition, TableExporter::MODE_DOWN), 297 | ]), 298 | ]; 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/Generator/MigrationMode.php: -------------------------------------------------------------------------------- 1 | setTypeKey($relation->getMorphType()); 43 | } else { 44 | /** @var DirectRelationship */ 45 | $migratable = App::make(DirectRelationship::class); 46 | } 47 | 48 | $relatedModel = $relation->getRelated(); 49 | $migratable->setRelatedTable((new $relatedModel())->getTable()); 50 | $migratable->setForeignKey($relation->getForeignKeyName()); 51 | 52 | $parentModel = $relation->getParent(); 53 | $migratable->setParentTable((new $parentModel())->getTable()); 54 | $migratable->setLocalKey($relation->getLocalKeyName()); 55 | 56 | return $migratable; 57 | } 58 | 59 | protected static function resolveHasOneOrManyThrough(HasOneOrManyThrough $relation): array 60 | { 61 | $throughModel = $relation->getParent(); 62 | $relatedModel = $relation->getRelated(); 63 | $farParentTable = explode('.', $relation->getQualifiedLocalKeyName())[0]; 64 | 65 | return [ 66 | App::make(DirectRelationship::class, [ 67 | 'parentTable' => (new $throughModel())->getTable(), 68 | 'relatedTable' => (new $relatedModel())->getTable(), 69 | 'foreignKey' => $relation->getForeignKeyName(), 70 | 'localKey' => $relation->getSecondLocalKeyName() 71 | 72 | ]), 73 | App::make(DirectRelationship::class, [ 74 | 'parentTable' => $farParentTable, 75 | 'relatedTable' => (new $throughModel())->getTable(), 76 | 'foreignKey' => $relation->getFirstKeyName(), 77 | 'localKey' => $relation->getLocalKeyName() 78 | ]), 79 | ]; 80 | } 81 | 82 | protected static function resolveBelongsTo(BelongsTo $relation): DirectRelationship 83 | { 84 | if ($relation instanceof MorphTo) { 85 | /** @var MorphicDirectRelationship */ 86 | $migratable = App::make(MorphicDirectRelationship::class); 87 | $migratable->setTypeKey($relation->getMorphType()); 88 | } else { 89 | /** @var DirectRelationship */ 90 | $migratable = App::make(DirectRelationship::class); 91 | 92 | $relatedModel = $relation->getRelated(); 93 | $migratable->setParentTable((new $relatedModel())->getTable()); 94 | } 95 | 96 | $parentModel = $relation->getParent(); 97 | $migratable->setRelatedTable((new $parentModel())->getTable()); 98 | 99 | $migratable->setForeignKey($relation->getForeignKeyName()); 100 | 101 | $localKey = $relation->getOwnerKeyName(); 102 | if (null !== $localKey) { 103 | $migratable->setLocalKey($localKey); 104 | } 105 | 106 | return $migratable; 107 | } 108 | 109 | protected static function resolveBelongsToMany(BelongsToMany $relation): IndirectRelationship 110 | { 111 | if ($relation instanceof MorphToMany) { 112 | /** @var MorphicIndirectRelationship */ 113 | $migratable = App::make(MorphicIndirectRelationship::class); 114 | $migratable->setTypeKey($relation->getMorphType()); 115 | } else { 116 | /** @var IndirectRelationship */ 117 | $migratable = App::make(IndirectRelationship::class); 118 | } 119 | 120 | $migratable->setPivotTable($relation->getTable()); 121 | 122 | $parentModel = $relation->getParent(); 123 | $parentTable = (new $parentModel())->getTable(); 124 | 125 | $migratable->addRelatedTable($parentTable); 126 | $migratable->addForeignKey($parentTable, $relation->getForeignPivotKeyName()); 127 | $migratable->addLocalKey($parentTable, $relation->getParentKeyName()); 128 | 129 | $relatedModel = $relation->getRelated(); 130 | $relatedTable = (new $relatedModel())->getTable(); 131 | 132 | $migratable->addRelatedTable($relatedTable); 133 | $migratable->addForeignKey($relatedTable, $relation->getRelatedPivotKeyName()); 134 | $migratable->addLocalKey($relatedTable, $relation->getRelatedKeyName()); 135 | 136 | $migratable->setPivotColumns($relation->getPivotColumns()); 137 | 138 | return $migratable; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Generator/TemplateManager.php: -------------------------------------------------------------------------------- 1 | template = file_get_contents(realpath($templatePath)); 21 | } 22 | 23 | public static function substitute(string $subject, string $key, string $value): string 24 | { 25 | $placeholder = "<<{$key}>>"; 26 | $placeholderPosition = strpos($subject, $placeholder); 27 | $previousNewlinePosition = strrpos($subject, "\n", $placeholderPosition - strlen($subject)); 28 | $afterNewline = false === $previousNewlinePosition ? '' : substr($subject, $previousNewlinePosition + 1); 29 | 30 | preg_match('/^\s+/', $afterNewline, $matches); 31 | $indentation = $matches[0] ?? ''; 32 | 33 | $value = strtr($value, [ 34 | "\t" => str_repeat(' ', static::TAB_SIZE), 35 | "\n" => "\n{$indentation}", 36 | ]); 37 | 38 | /** @var string */ 39 | $value = preg_replace('/\n\s+\n/', "\n\n", $value); 40 | 41 | return str_replace($placeholder, $value, $subject); 42 | } 43 | 44 | /** 45 | * @param array $data 46 | * @return string 47 | */ 48 | public function process(array $data): string 49 | { 50 | $subject = $this->template; 51 | 52 | foreach ($data as $key => $value) { 53 | $subject = static::substitute($subject, $key, $value); 54 | } 55 | 56 | return $subject; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Providers/ImplicitMigrationsServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->runningInConsole()) { 48 | $this->commands([ 49 | GenerateMigrationCommand::class, 50 | ]); 51 | } 52 | 53 | $this->publishes([ 54 | implode(DIRECTORY_SEPARATOR, [__DIR__, '..', 'Attributes']) => database_path('attributes'), 55 | ], 'implication-attributes'); 56 | } 57 | 58 | public function register(): void 59 | { 60 | $this->mergeConfigFrom(implode(DIRECTORY_SEPARATOR, [ 61 | __DIR__, 62 | '..', 63 | 'config', 64 | 'database.php' 65 | ]), 'database'); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/config/database.php: -------------------------------------------------------------------------------- 1 | [ 5 | 'app' . DIRECTORY_SEPARATOR . 'Models' 6 | ], 7 | 'auto_infer_migrations' => true, 8 | 'implications' => [ 9 | 'table' => true, 10 | 'column' => true, 11 | 'char' => true, 12 | 'string' => true, 13 | 'integer' => true, 14 | 'tinyInteger' => true, 15 | 'smallInteger' => true, 16 | 'mediumInteger' => true, 17 | 'bigInteger' => true, 18 | 'unsignedInteger' => true, 19 | 'unsignedTinyInteger' => true, 20 | 'unsignedSmallInteger' => true, 21 | 'unsignedMediumInteger' => true, 22 | 'unsignedBigInteger' => true, 23 | 'increments' => true, 24 | 'tinyIncrements' => true, 25 | 'smallIncrements' => true, 26 | 'mediumIncrements' => true, 27 | 'bigIncrements' => true, 28 | 'float' => true, 29 | 'decimal' => true, 30 | 'enum' => true, 31 | 'set' => true, 32 | 'time' => true, 33 | 'timeTz' => true, 34 | 'timestamp' => true, 35 | 'timestampTz' => true, 36 | 'dateTime' => true, 37 | 'dateTimeTz' => true, 38 | 'binary' => true, 39 | 'geometry' => true, 40 | 'geography' => true, 41 | 'computed' => true, 42 | 'index' => true, 43 | 'unique' => true, 44 | 'primary' => true, 45 | 'foreign_key' => true, 46 | 'relationship' => true, 47 | 'pivot_table' => true, 48 | 'pivot_column' => true, 49 | 'off' => true, 50 | ], 51 | ]; 52 | -------------------------------------------------------------------------------- /src/templates/migration-create.php.tpl: -------------------------------------------------------------------------------- 1 | > as Source; 7 | 8 | return new class extends Migration 9 | { 10 | public const TABLE_NAME = '<>'; 11 | 12 | public function getSource(): string 13 | { 14 | return <>; 15 | } 16 | 17 | public function tableUp(Blueprint $table): void 18 | { 19 | <> 20 | } 21 | 22 | public function up(): void 23 | { 24 | Schema::create(static::TABLE_NAME, function (Blueprint $table) { 25 | $this->tableUp($table); 26 | }); 27 | } 28 | 29 | public function down(): void 30 | { 31 | Schema::drop(static::TABLE_NAME); 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /src/templates/migration-update.php.tpl: -------------------------------------------------------------------------------- 1 | > as Source; 7 | 8 | return new class extends Migration 9 | { 10 | public const TABLE_NAME = '<>'; 11 | 12 | public function getSource(): string 13 | { 14 | return <>; 15 | } 16 | 17 | public function tableUp(Blueprint $table): void 18 | { 19 | <> 20 | } 21 | 22 | public function tableDown(Blueprint $table): void 23 | { 24 | <> 25 | } 26 | 27 | public function up(): void 28 | { 29 | Schema::table(<>, function (Blueprint $table) { 30 | $this->tableUp($table); 31 | }); 32 | } 33 | 34 | public function down(): void 35 | { 36 | Schema::table(static::TABLE_NAME, function (Blueprint $table) { 37 | $this->tableDown($table); 38 | }); 39 | } 40 | }; 41 | --------------------------------------------------------------------------------