Repository: spatie/laravel-permission (php)

Issue:
Columns not available for extended Permissions model
**Describe the bug**
Hi, first of all, I'm sorry if this is not a bug, but I'm having a hard time trying to get my app working with the latest release, so I'm not sure if it's a specific problem from my use case or if it's general.
Basically, recent releases kind of "broke" backwards compatibility with some features that are still documented as available, such as the ability to extend the models.
On my application I've extended both models (Permissions and Roles), mainly to have an extra attribute for the permissions (my app is divided between modules, so I have a `module_id` column on my permissions table that I double check, to make sure users can't use permissions of modules they don't have, build custom menus and so on).
I haven't updated the package for a few months now, and when I tried to update today, I encountered this issue, apparently in order to shrink the cache, the package has now the attributes of the permissions model hard-coded on the PermissionRegistrar, so I can't check for any other columns I have on the DB because they will never be available.
I'm not sure if this was intended, but if so, I guess we should update the docs and possibly document it as a breaking change (?).

**Versions**
- spatie/laravel-permission package version: 5.1.1
- illuminate/framework package version: 8.58.0

PHP version:
8.0.10

Database version:
MySQL 8.0.23

**To Reproduce**
Just add a new column to the permissions table and try to access it anywhere on the app using a method such as the `findByName` from the Permission's model.

**Expected behavior**
I guess it would be nice if we could have the attributes we want to cache on the config or something, so we can still shrink the cache in a way, but also have the flexibility to have extra columns, if needed.
 SQLSTATE[42S22]: Column not found: 1054 Unknown column 'name' in 'field list'
### Code

$administratorRole = **OBJECT**;
$permissions = **ARRAY OF PERMISSIONS**;
$administratorRole->syncPermissions($permissions);

~~~sql
 SQLSTATE[42S22]: Column not found: 1054 Unknown column 'name' in 'field list' (SQL: select `id`, `id` as `i`, `name` as `n`, `guard_name` as `g` from `permissions`)
~~~

![image](https://user-images.githubusercontent.com/12006483/153721484-24c41d93-1dba-4f4e-8107-de346a141758.png)
![image](https://user-images.githubusercontent.com/12006483/153721501-1ef06512-6d7a-48bc-85b4-6cefaa302756.png)

#### Composer 
"spatie/laravel-permission": "^5.5",

#### PHP
PHP 7.4.27 (cli) (built: Dec 20 2021 21:28:15) ( NTS )

Can any one help on this !Thanks in advance

Pull request:
Support custom fields on cache
**Not breaking change**
Closes #1836
Closes #2046
Closes #2016

Set all models fields on cache, still using alias and arrays for smaller cache, added automatic alias for avoid using `i,n,g`, 
`i,n,g` causes a problem when you add a custom field like `type`, `description`, `label`

**Before:**
https://github.com/spatie/laravel-permission/blob/b71df07f3b5db4886657b4c17244d66ba6796c1a/src/PermissionRegistrar.php#L282-L285
https://github.com/spatie/laravel-permission/blob/b71df07f3b5db4886657b4c17244d66ba6796c1a/src/PermissionRegistrar.php#L341-L345
**After:** Less code, easier to understand
```php
$permissions = $this->getPermissionClass()->select()->with('roles')->get()
///
$role = $roleInstance->newFromBuilder($this->aliasedArray($item));
$this->cachedRoles[$role->getKey()] = $role;
```

------
**Results:** 
```php
array:358 [▼
  0 => array:8 [▼
    "i" => 197
    "n" => "view_audit"
    "g" => "web"
    "l" => "Audit"
    "e" => null
    "y" => "A"
    "r" => array:3 [▼
      0 => array:5 [▼
        "i" => 0
        "t" => null
        "n" => "System"
        "g" => "web"
        "y" => "A"
      ]
```
`1631207353a:358:{i:0;a:8:{s:1:"i";i:197;s:1:"n";s:10:"view_audit";s:1:"g";s:3:"web";s:1:"l";s:10:"Audit";s:1:"e";N;s:1:"y";s:1:"A";s:1:"r";a:3:{i:0;a:5:{s:1:"i";i:0;s:1:"t";N;s:1:"n";s:8:"System";s:1:"g";s:3:"web";s:1:"y";s:1:"A";}i:`

Added test `it_can_use_custom_fields_from_cache`, checked not over hydration

![image](https://user-images.githubusercontent.com/4933954/167211143-ef492268-f835-4e88-b4b7-dca8fb1b7678.png)



Patch:
diff --git a/docs/advanced-usage/uuid.md b/docs/advanced-usage/uuid.md
index bebbeef..299f83c 100644
--- a/docs/advanced-usage/uuid.md
+++ b/docs/advanced-usage/uuid.md
@@ -96,10 +96,8 @@ It is common to use a trait to handle the $keyType and $incrementing settings, a
 
     trait UuidTrait
     {
-        protected static function bootUuidTrait()
+        public static function bootUuidTrait()
         {
-            parent::boot();
-
             static::creating(function ($model) {
                 $model->keyType = 'string';
                 $model->incrementing = false;
diff --git a/src/Models/Permission.php b/src/Models/Permission.php
index 2b253d6..1834d72 100644
--- a/src/Models/Permission.php
+++ b/src/Models/Permission.php
@@ -108,7 +108,7 @@ class Permission extends Model implements PermissionContract
     public static function findById(int $id, $guardName = null): PermissionContract
     {
         $guardName = $guardName ?? Guard::getDefaultName(static::class);
-        $permission = static::getPermission(['id' => $id, 'guard_name' => $guardName]);
+        $permission = static::getPermission([(new static())->getKeyName() => $id, 'guard_name' => $guardName]);
 
         if (! $permission) {
             throw PermissionDoesNotExist::withId($id, $guardName);
diff --git a/src/Models/Role.php b/src/Models/Role.php
index 76bec30..da6f269 100644
--- a/src/Models/Role.php
+++ b/src/Models/Role.php
@@ -115,7 +115,7 @@ class Role extends Model implements RoleContract
     {
         $guardName = $guardName ?? Guard::getDefaultName(static::class);
 
-        $role = static::findByParam(['id' => $id, 'guard_name' => $guardName]);
+        $role = static::findByParam([(new static())->getKeyName() => $id, 'guard_name' => $guardName]);
 
         if (! $role) {
             throw RoleDoesNotExist::withId($id);
diff --git a/src/PermissionRegistrar.php b/src/PermissionRegistrar.php
index ce10baf..38beec6 100644
--- a/src/PermissionRegistrar.php
+++ b/src/PermissionRegistrar.php
@@ -52,6 +52,12 @@ class PermissionRegistrar
     /** @var array */
     private $cachedRoles = [];
 
+    /** @var array */
+    private $alias = [];
+
+    /** @var array */
+    private $except = [];
+
     /**
      * PermissionRegistrar constructor.
      *
@@ -174,18 +180,20 @@ class PermissionRegistrar
         });
 
         // fallback for old cache method, must be removed on next mayor version
-        if (! isset($this->permissions['permissions'])) {
+        if (! isset($this->permissions['alias'])) {
             $this->forgetCachedPermissions();
             $this->loadPermissions();
 
             return;
         }
 
+        $this->alias = $this->permissions['alias'];
+
         $this->hydrateRolesCache();
 
         $this->permissions = $this->getHydratedPermissionCollection();
 
-        $this->cachedRoles = [];
+        $this->cachedRoles = $this->alias = $this->except = [];
     }
 
     /**
@@ -267,27 +275,55 @@ class PermissionRegistrar
         return $this->cache->getStore();
     }
 
+    /**
+     * Changes array keys with alias
+     *
+     * @return array
+     */
+    private function aliasedArray($model): array
+    {
+        return collect(is_array($model) ? $model : $model->getAttributes())->except($this->except)
+            ->keyBy(function ($value, $key) {
+                return $this->alias[$key] ?? $key;
+            })->all();
+    }
+
+    /**
+     * Array for cache alias
+     */
+    private function aliasModelFields($newKeys = []): void
+    {
+        $i = 0;
+        $alphas = ! count($this->alias) ? range('a', 'h') : range('j', 'p');
+
+        foreach (array_keys($newKeys->getAttributes()) as $value) {
+            if (! isset($this->alias[$value])) {
+                $this->alias[$value] = $alphas[$i++] ?? $value;
+            }
+        }
+
+        $this->alias = array_diff_key($this->alias, array_flip($this->except));
+    }
+
     /*
      * Make the cache smaller using an array with only required fields
      */
     private function getSerializedPermissionsForCache()
     {
-        $roleClass = $this->getRoleClass();
-        $roleKey = (new $roleClass())->getKeyName();
-
-        $permissionClass = $this->getPermissionClass();
-        $permissionKey = (new $permissionClass())->getKeyName();
+        $this->except = config('permission.cache.column_names_except', ['created_at','updated_at', 'deleted_at']);
 
-        $permissions = $permissionClass
-            ->select($permissionKey, "$permissionKey as i", 'name as n', 'guard_name as g')
-            ->with("roles:$roleKey,$roleKey as i,name as n,guard_name as g")->get()
+        $permissions = $this->getPermissionClass()->select()->with('roles')->get()
             ->map(function ($permission) {
-                return $permission->only('i', 'n', 'g') + $this->getSerializedRoleRelation($permission);
+                if (! $this->alias) {
+                    $this->aliasModelFields($permission);
+                }
+
+                return $this->aliasedArray($permission) + $this->getSerializedRoleRelation($permission);
             })->all();
         $roles = array_values($this->cachedRoles);
         $this->cachedRoles = [];
 
-        return compact('permissions', 'roles');
+        return ['alias' => array_flip($this->alias)] + compact('permissions', 'roles');
     }
 
     private function getSerializedRoleRelation($permission)
@@ -296,13 +332,18 @@ class PermissionRegistrar
             return [];
         }
 
+        if (! isset($this->alias['roles'])) {
+            $this->alias['roles'] = 'r';
+            $this->aliasModelFields($permission->roles[0]);
+        }
+
         return [
             'r' => $permission->roles->map(function ($role) {
-                if (! isset($this->cachedRoles[$role->i])) {
-                    $this->cachedRoles[$role->i] = $role->only('i', 'n', 'g');
+                if (! isset($this->cachedRoles[$role->getKey()])) {
+                    $this->cachedRoles[$role->getKey()] = $this->aliasedArray($role);
                 }
 
-                return $role->i;
+                return $role->getKey();
             })->all(),
         ];
     }
@@ -315,11 +356,7 @@ class PermissionRegistrar
         return Collection::make(
             array_map(function ($item) use ($permissionInstance) {
                 return $permissionInstance
-                    ->newFromBuilder([
-                        $permissionInstance->getKeyName() => $item['i'],
-                        'name' => $item['n'],
-                        'guard_name' => $item['g'],
-                    ])
+                    ->newFromBuilder($this->aliasedArray(array_diff_key($item, ['r' => 0])))
                     ->setRelation('roles', $this->getHydratedRoleCollection($item['r'] ?? []));
             }, $this->permissions['permissions'])
         );
@@ -338,11 +375,8 @@ class PermissionRegistrar
         $roleInstance = new $roleClass();
 
         array_map(function ($item) use ($roleInstance) {
-            $this->cachedRoles[$item['i']] = $roleInstance->newFromBuilder([
-                $roleInstance->getKeyName() => $item['i'],
-                'name' => $item['n'],
-                'guard_name' => $item['g'],
-            ]);
+            $role = $roleInstance->newFromBuilder($this->aliasedArray($item));
+            $this->cachedRoles[$role->getKey()] = $role;
         }, $this->permissions['roles']);
     }
 }


Test patch:
diff --git a/tests/HasPermissionsWithCustomModelsTest.php b/tests/HasPermissionsWithCustomModelsTest.php
index bdb237a..6b9fa34 100644
--- a/tests/HasPermissionsWithCustomModelsTest.php
+++ b/tests/HasPermissionsWithCustomModelsTest.php
@@ -2,6 +2,9 @@
 
 namespace Spatie\Permission\Test;
 
+use DB;
+use Spatie\Permission\PermissionRegistrar;
+
 class HasPermissionsWithCustomModelsTest extends HasPermissionsTest
 {
     /** @var bool */
@@ -12,4 +15,25 @@ class HasPermissionsWithCustomModelsTest extends HasPermissionsTest
     {
         $this->assertSame(get_class($this->testUserPermission), Permission::class);
     }
+
+    /** @test */
+    public function it_can_use_custom_fields_from_cache()
+    {
+        DB::connection()->getSchemaBuilder()->table(config('permission.table_names.roles'), function ($table) {
+            $table->string('type')->default('R');
+        });
+        DB::connection()->getSchemaBuilder()->table(config('permission.table_names.permissions'), function ($table) {
+            $table->string('type')->default('P');
+        });
+
+        $this->testUserRole->givePermissionTo($this->testUserPermission);
+        app(PermissionRegistrar::class)->getPermissions();
+
+        DB::enableQueryLog();
+        $this->assertSame('P', Permission::findByName('edit-articles')->type);
+        $this->assertSame('R', Permission::findByName('edit-articles')->roles[0]->type);
+        DB::disableQueryLog();
+
+        $this->assertSame(0, count(DB::getQueryLog()));
+    }
 }