Repository: spatie/laravel-permission (php)

Issue:
Teams permission setup migration fails after upgrading from 4.*
**Before creating a new bug report**
Please check if there isn't a similar issue on [the issue tracker](https://github.com/spatie/laravel-permission/issues) or in [the discussions](https://github.com/spatie/laravel-permission/discussions).

**Describe the bug**
After upgrading to 5.* and setting up Teams functionality, migration created by `php artisan permission:setup-teams` fails to rename two primary keys.
On first run of `artisan migrate` I get
```SQLSTATE[HY000]: General error: 1025 Error on rename of './forge/#sql-alter-1-643' to './forge/model_has_permissions' (errno: 150 "Foreign key constraint is incorrectly formed") (SQL: alter table `model_has_permissions` drop primary key)```

On second run it doesn't try to rename same key, as required columns are there, but it fails on another one:
```SQLSTATE[HY000]: General error: 1025 Error on rename of './forge/#sql-alter-1-659' to './forge/model_has_roles' (errno: 150 "Foreign key constraint is incorrectly formed") (SQL: alter table `model_has_roles` drop primary key)```.

On third run migration is successful, but primary keys keys in database are unchanged. This might cause reduced performance as pre-migration primary keys are in the database and they don't contain `team_id` column.

Adding `Schema::disableForeignKeyConstraints();` to migration fixes this issue.

**Versions**
You can use `composer show` to get the version numbers of:
- spatie/laravel-permission package version: 5.4.0
- **laravel**/framework package: v8.76.1

PHP version: 8.1

Database version: MariaDB 10.6.5

**To Reproduce**
Steps to reproduce the behavior:

- Upgrade from 4.* to 5.*
- Update config to use Teams
- Run `php artisan permission:setup-teams`
- Run `php artisan migrate`

Here is my example code and/or tests showing the problem in my app:
Migration created by `php artisan permission:setup-teams` :
```
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class AddTeamsFields extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        $teams = config('permission.teams');
        $tableNames = config('permission.table_names');
        $columnNames = config('permission.column_names');

        if (! $teams) {
            return;
        }
        if (empty($tableNames)) {
            throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
        }
        if (empty($columnNames['team_foreign_key'] ?? null)) {
            throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
        }

        Schema::table($tableNames['roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
            if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
                $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
                $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
            }
        });

        Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
            if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
                $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
                $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');

                $table->dropPrimary();
                $table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'],
                    'model_has_permissions_permission_model_type_primary');
            }
        });

        Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
            if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
                $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
                $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');

                $table->dropPrimary();
                $table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'],
                    'model_has_roles_role_model_type_primary');
            }
        });

        app('cache')
            ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
            ->forget(config('permission.cache.key'));
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {

    }
}
```

**Example Application**
Will create if needed. 

**Expected behavior**
Migration should be successful.

**Environment (please complete the following information, because it helps us investigate better):**
 - OS: WSL2 Debian + Laravel Sail
 - Version [e.g. 22]
 


Pull request:
Fix teams upgrade migration
Closes #1957

- Avoid `"Foreign key constraint is incorrectly formed"` described on #1957
- Support for custom pivots(`PermissionRegistrar::$pivotRole`, `PermissionRegistrar::$pivotPermission`)


**To Reproduce Bug**
Steps to reproduce the behavior:

- Delete `xxxx_xx_xx_xxxxxx_add_teams_fields.php` from `database/migrations`
- Update config to disable Teams `'teams' => false,`
- Run `php artisan migrate:fresh`
- Update config to enable Teams `'teams' => true,`
- Run `php artisan permission:setup-teams`
- Run `php artisan migrate`

https://github.com/spatie/laravel-permission/blob/08cc9bdbf89640cb1d64fb01eb9513c1cbb2549d/tests/CommandTest.php#L135-L159

Patch:
diff --git a/.gitattributes b/.gitattributes
index 7742c9a..5a0aa16 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -2,11 +2,15 @@
 # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html
 
 # Ignore all test and documentation with "export-ignore".
+/.github            export-ignore
 /.gitattributes     export-ignore
 /.gitignore         export-ignore
 /.travis.yml        export-ignore
 /phpunit.xml.dist   export-ignore
 /.scrutinizer.yml   export-ignore
+/art                export-ignore
+/docs               export-ignore
 /tests              export-ignore
 /.editorconfig      export-ignore
+/.php_cs.dist.php   export-ignore
 /.styleci.yml       export-ignore
diff --git a/database/migrations/add_teams_fields.php.stub b/database/migrations/add_teams_fields.php.stub
index 727104f..55d2afc 100644
--- a/database/migrations/add_teams_fields.php.stub
+++ b/database/migrations/add_teams_fields.php.stub
@@ -1,8 +1,10 @@
 <?php
 
+use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Schema;
 use Illuminate\Database\Schema\Blueprint;
 use Illuminate\Database\Migrations\Migration;
+use Spatie\Permission\PermissionRegistrar;
 
 class AddTeamsFields extends Migration
 {
@@ -27,34 +29,50 @@ class AddTeamsFields extends Migration
             throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
         }
 
-        Schema::table($tableNames['roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
-            if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
+        if (! Schema::hasColumn($tableNames['roles'], $columnNames['team_foreign_key'])) {
+            Schema::table($tableNames['roles'], function (Blueprint $table) use ($columnNames) {
                 $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
                 $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
-            }
-        });
+            });
+        }
 
-        Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
-            if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
+        if (! Schema::hasColumn($tableNames['model_has_permissions'], $columnNames['team_foreign_key'])) {
+            Schema::table($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames) {
                 $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
                 $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
 
+                if (DB::getDriverName() !== 'sqlite') {
+                    $table->dropForeign([PermissionRegistrar::$pivotPermission]);
+                }
                 $table->dropPrimary();
-                $table->primary([$columnNames['team_foreign_key'], 'permission_id', $columnNames['model_morph_key'], 'model_type'],
+
+                $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
                     'model_has_permissions_permission_model_type_primary');
-            }
-        });
+                if (DB::getDriverName() !== 'sqlite') {
+                    $table->foreign(PermissionRegistrar::$pivotPermission)
+                        ->references('id')->on($tableNames['permissions'])->onDelete('cascade');
+                }
+            });
+        }
 
-        Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
-            if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
+        if (! Schema::hasColumn($tableNames['model_has_roles'], $columnNames['team_foreign_key'])) {
+            Schema::table($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames) {
                 $table->unsignedBigInteger($columnNames['team_foreign_key'])->default('1');;
                 $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
 
+                if (DB::getDriverName() !== 'sqlite') {
+                    $table->dropForeign([PermissionRegistrar::$pivotRole]);
+                }
                 $table->dropPrimary();
-                $table->primary([$columnNames['team_foreign_key'], 'role_id', $columnNames['model_morph_key'], 'model_type'],
+
+                $table->primary([$columnNames['team_foreign_key'], PermissionRegistrar::$pivotRole, $columnNames['model_morph_key'], 'model_type'],
                     'model_has_roles_role_model_type_primary');
-            }
-        });
+                if (DB::getDriverName() !== 'sqlite') {
+                    $table->foreign(PermissionRegistrar::$pivotRole)
+                        ->references('id')->on($tableNames['roles'])->onDelete('cascade');
+                }
+            });
+        }
 
         app('cache')
             ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
diff --git a/docs/advanced-usage/ui-options.md b/docs/advanced-usage/ui-options.md
index 787804e..e8029ff 100644
--- a/docs/advanced-usage/ui-options.md
+++ b/docs/advanced-usage/ui-options.md
@@ -18,3 +18,6 @@ The package doesn't come with any screens out of the box, you should build that
 - [Laravel User Management for managing users, roles, permissions, departments and authorization](https://github.com/Mekaeil/LaravelUserManagement) by [Mekaeil](https://github.com/Mekaeil)
 
 - [Generating UI boilerplate using InfyOm](https://youtu.be/hlGu2pa1bdU) video tutorial by [Shailesh](https://github.com/shailesh-ladumor)
+
+
+- [LiveWire Base Admin Panel](https://github.com/alighasemzadeh/bap) User management by [AliGhasemzadeh](https://github.com/alighasemzadeh)


Test patch:
diff --git a/docs/advanced-usage/testing.md b/docs/advanced-usage/testing.md
index bd9e64c..d0cd3e7 100644
--- a/docs/advanced-usage/testing.md
+++ b/docs/advanced-usage/testing.md
@@ -20,6 +20,17 @@ In your tests simply add a `setUp()` instruction to re-register the permissions,
     }
 ```
 
+If you are using Laravel's `LazilyRefreshDatabase` trait, you most likely want to avoid seeding permissions before every test, because that would negate the use of the `LazilyRefreshDatabase` trait. To overcome this, you should wrap your seeder in an event listener for the `MigrationsEnded` event:
+
+```php
+Event::listen(MigrationsEnded::class, function () {
+    $this->artisan('db:seed', ['--class' => RoleAndPermissionSeeder::class]);
+    $this->app->make(\Spatie\Permission\PermissionRegistrar::class)->forgetCachedPermissions();
+});
+```
+
+Note that we call `PermissionRegistrar::forgetCachedPermissions` after seeding. This is to prevent a caching issue that can occur when the database is set up after permissions have already been registered and cached. 
+
 ## Factories
 
 Many applications do not require using factories to create fake roles/permissions for testing, because they use a Seeder to create specific roles and permissions that the application uses; thus tests are performed using the declared roles and permissions.