Repository: laravel-shift/blueprint (php)

Issue:
Problem is with two-word model names
<!-- PLEASE COMPLETE THIS TEMPLATE -->

- Laravel Version: 7
- PHP Version: 7.4
- Blueprint Version: 1.18
- Platform: Linux 

### Issue: In result I get such a table for relationship n-n:

```
        Schema::create('broker_broker_type', function (Blueprint $table) {
            $table->foreignId('broker_id');
            $table->foreignId('brokertype_id');
        });
```
The problem is with two-word model names. The second field has 'brokertype_id' name, but not 'broker_type_id'. But lavarel's backback, for example, is searching for the 'broker_type_id' field of the BrokerType model.

Will be great if Blueprint generate '_' in the fields that were processed from two-word model names. Or if it could be customizable.

P.S: Thanks for the great tool!

### draft.yaml:
```
models:
  Models\Broker:
    name: string
    established: integer unsigned nullable
    location: string
    offices: string
    max_leverage: integer unsigned
    min_deposit: integer unsigned nullable
    spreads: integer unsigned
    logo: string
    text_company: text
    text_availability: text
    text_features: text
    text_instruments: text
    text_review: text
    timestamps: true
    softDeletes: true
    relationships:
      belongsToMany: BrokerType

  Models\BrokerType:
    name: string
    slug: string unique
    timestamps: true
    softDeletes: true
    relationships:
      belongsToMany: Broker
```


Pull request description:
Refactor MigrationGenerator class
Currently:
---
- There's a bug with two-word model names #374 
- The last 2 migrations generated always have the same timestamp.

PR
---
- fix #374 
- refactor: migration generator class 
    - create migrations sequential with unique `timestamp`.
    - create `modelTables` before `pivotTables`
    - remove dead code
- update migration generator `fixtures, stubs, and tests`

Patch:
diff --git a/src/Generators/MigrationGenerator.php b/src/Generators/MigrationGenerator.php
index 658a0da..870f4a6 100644
--- a/src/Generators/MigrationGenerator.php
+++ b/src/Generators/MigrationGenerator.php
@@ -49,37 +49,23 @@ class MigrationGenerator implements Generator
 
     public function output(Tree $tree, $overwrite = false): array
     {
-        $created_pivot_tables = [];
+        $tables = ['tableNames' => [], 'pivotTableNames' => []];
 
         $stub = $this->files->stub('migration.stub');
 
-        $sequential_timestamp = \Carbon\Carbon::now()->subSeconds(count($tree->models()));
-
         /** @var \Blueprint\Models\Model $model */
         foreach ($tree->models() as $model) {
-            $path = $this->getPath($model, $sequential_timestamp->addSecond(), $overwrite);
-            $action = $this->files->exists($path) ? 'updated' : 'created';
-            $this->files->put($path, $this->populateStub($stub, $model));
-
-            $this->output[$action][] = $path;
+            $tables['tableNames'][$model->tableName()] = $this->populateStub($stub, $model);
 
             if (! empty($model->pivotTables())) {
                 foreach ($model->pivotTables() as $pivotSegments) {
-                    $pivotTable = $this->getPivotTableName($pivotSegments);
-                    $created_pivot_tables[$pivotTable] = $pivotSegments;
+                    $pivotTableName = $this->getPivotTableName($pivotSegments);
+                    $tables['pivotTableNames'][$pivotTableName] = $this->populatePivotStub($stub, $pivotSegments);
                 }
             }
         }
 
-        foreach ($created_pivot_tables as $pivotTable => $pivotSegments) {
-            $path = $this->getPivotTablePath($pivotTable, $sequential_timestamp, $overwrite);
-            $action = $this->files->exists($path) ? 'updated' : 'created';
-            $this->files->put($path, $this->populatePivotStub($stub, $pivotSegments));
-            $created_pivot_tables[] = $pivotTable;
-            $this->output[$action][] = $path;
-        }
-
-        return $this->output;
+        return $this->createMigrations($tables, $overwrite);
     }
 
     public function types(): array
@@ -87,6 +73,33 @@ class MigrationGenerator implements Generator
         return ['migrations'];
     }
 
+    protected function createMigrations(array $tables, $overwrite = false): array
+    {
+        $output = [];
+
+        $sequential_timestamp = \Carbon\Carbon::now()->copy()->subSeconds(
+            collect($tables['tableNames'])->merge($tables['pivotTableNames'])->count()
+        );
+
+        foreach ($tables['tableNames'] as $tableName => $data) {
+            $path = $this->getTablePath($tableName, $sequential_timestamp->addSecond(), $overwrite);
+            $action = $this->files->exists($path) ? 'updated' : 'created';
+            $this->files->put($path, $data);
+
+            $output[$action][] = $path;
+        }
+
+        foreach ($tables['pivotTableNames'] as $tableName => $data) {
+            $path = $this->getTablePath($tableName, $sequential_timestamp->addSecond(), $overwrite);
+            $action = $this->files->exists($path) ? 'updated' : 'created';
+            $this->files->put($path, $data);
+
+            $output[$action][] = $path;
+        }
+
+        return $output;
+    }
+
     protected function populateStub(string $stub, Model $model)
     {
         $stub = str_replace('{{ class }}', $this->getClassName($model), $stub);
@@ -234,7 +247,7 @@ class MigrationGenerator implements Generator
         $definition = '';
 
         foreach ($segments as $segment) {
-            $column = Str::before(Str::lower($segment), ':');
+            $column = Str::before(Str::snake($segment), ':');
             $references = 'id';
             $on = Str::plural($column);
             $foreign = Str::singular($column).'_'.$references;
@@ -315,11 +328,6 @@ class MigrationGenerator implements Generator
         return $this->getTablePath($model->tableName(), $timestamp, $overwrite);
     }
 
-    protected function getPivotTablePath($tableName, Carbon $timestamp, $overwrite = false)
-    {
-        return $this->getTablePath($tableName, $timestamp, $overwrite);
-    }
-
     protected function getTablePath($tableName, Carbon $timestamp, $overwrite = false)
     {
         $dir = 'database/migrations/';


Test patch:
diff --git a/tests/Feature/Generators/MigrationGeneratorTest.php b/tests/Feature/Generators/MigrationGeneratorTest.php
index 12f25b8..88cb68d 100644
--- a/tests/Feature/Generators/MigrationGeneratorTest.php
+++ b/tests/Feature/Generators/MigrationGeneratorTest.php
@@ -162,6 +162,37 @@ class MigrationGeneratorTest extends TestCase
         $this->assertEquals(['created' => [$post_path, $comment_path]], $this->subject->output($tree));
     }
 
+    /**
+     * @test
+     */
+    public function output_proper_pascal_case_model_names()
+    {
+        $this->files->expects('stub')
+            ->with('migration.stub')
+            ->andReturn($this->stub('migration.stub'));
+
+        $now = Carbon::now();
+        Carbon::setTestNow($now);
+
+        $broker_path = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_brokers_table.php');
+        $broker_type_path = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_broker_types_table.php');
+        $broker_broker_type_path = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_broker_broker_type_table.php');
+
+        $this->files->expects('exists')->times(3)->andReturn(false);
+
+        $this->files->expects('put')
+            ->with($broker_path, $this->fixture('migrations/pascal-case-model-names-broker.php'));
+        $this->files->expects('put')
+            ->with($broker_type_path, $this->fixture('migrations/pascal-case-model-names-broker-type.php'));
+        $this->files->expects('put')
+            ->with($broker_broker_type_path, $this->fixture('migrations/pascal-case-model-names-broker-broker-type.php'));
+
+        $tokens = $this->blueprint->parse($this->fixture('drafts/pascal-case-model-names.yaml'));
+        $tree = $this->blueprint->analyze($tokens);
+
+        $this->assertEquals(['created' => [$broker_path, $broker_type_path, $broker_broker_type_path]], $this->subject->output($tree));
+    }
+
     /**
      * @test
      * @environment-setup useLaravel6
@@ -255,7 +286,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
@@ -320,7 +351,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
@@ -351,7 +382,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
@@ -383,7 +414,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_journeys_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_diary_journey_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
@@ -411,8 +442,8 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $company_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_companies_table.php');
-        $people_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_people_table.php');
+        $company_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_companies_table.php');
+        $people_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_people_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_company_person_table.php');
 
         $this->files->expects('exists')->times(3)->andReturn(false);
@@ -443,8 +474,8 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $company_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_companies_table.php');
-        $people_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_people_table.php');
+        $company_migration = str_replace('timestamp', $now->copy()->subSeconds(2)->format('Y_m_d_His'), 'database/migrations/timestamp_create_companies_table.php');
+        $people_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_people_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_company_person_table.php');
 
         $this->files->expects('exists')->times(3)->andReturn(false);
@@ -474,7 +505,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_test_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
@@ -503,7 +534,7 @@ class MigrationGeneratorTest extends TestCase
         $now = Carbon::now();
         Carbon::setTestNow($now);
 
-        $model_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
+        $model_migration = str_replace('timestamp', $now->copy()->subSecond()->format('Y_m_d_His'), 'database/migrations/timestamp_create_users_table.php');
         $pivot_migration = str_replace('timestamp', $now->format('Y_m_d_His'), 'database/migrations/timestamp_create_test_table.php');
 
         $this->files->expects('exists')->twice()->andReturn(false);
diff --git a/tests/fixtures/drafts/pascal-case-model-names.yaml b/tests/fixtures/drafts/pascal-case-model-names.yaml
new file mode 100644
index 0000000..fb5480f
--- /dev/null
+++ b/tests/fixtures/drafts/pascal-case-model-names.yaml
@@ -0,0 +1,10 @@
+models:
+  Broker:
+    name: string
+    relationships:
+      belongsToMany: BrokerType
+
+  BrokerType:
+    name: string
+    relationships:
+      belongsToMany: Broker
diff --git a/tests/fixtures/migrations/pascal-case-model-names-broker-broker-type.php b/tests/fixtures/migrations/pascal-case-model-names-broker-broker-type.php
new file mode 100644
index 0000000..36455a5
--- /dev/null
+++ b/tests/fixtures/migrations/pascal-case-model-names-broker-broker-type.php
@@ -0,0 +1,31 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateBrokerBrokerTypeTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('broker_broker_type', function (Blueprint $table) {
+            $table->foreignId('broker_id');
+            $table->foreignId('broker_type_id');
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('broker_broker_type');
+    }
+}
diff --git a/tests/fixtures/migrations/pascal-case-model-names-broker-type.php b/tests/fixtures/migrations/pascal-case-model-names-broker-type.php
new file mode 100644
index 0000000..bf63857
--- /dev/null
+++ b/tests/fixtures/migrations/pascal-case-model-names-broker-type.php
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateBrokerTypesTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('broker_types', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('broker_types');
+    }
+}
diff --git a/tests/fixtures/migrations/pascal-case-model-names-broker.php b/tests/fixtures/migrations/pascal-case-model-names-broker.php
new file mode 100644
index 0000000..654431f
--- /dev/null
+++ b/tests/fixtures/migrations/pascal-case-model-names-broker.php
@@ -0,0 +1,32 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+class CreateBrokersTable extends Migration
+{
+    /**
+     * Run the migrations.
+     *
+     * @return void
+     */
+    public function up()
+    {
+        Schema::create('brokers', function (Blueprint $table) {
+            $table->id();
+            $table->string('name');
+            $table->timestamps();
+        });
+    }
+
+    /**
+     * Reverse the migrations.
+     *
+     * @return void
+     */
+    public function down()
+    {
+        Schema::dropIfExists('brokers');
+    }
+}