I think this package needs a memory cache because everything is unserialized from external cache every time hasPermissionTo is called.
I have a system with 100+ permissions and roles (the cache file is 4.6mb) and a view with 75+ @can directives, the page was taking more than 20 seconds to load.
A simple solution like this got it down to 2 seconds.
// PermissionRegistrar.php
private $permissions;
public function getPermissions(): Collection
{
if (!$this->permissions) {
$this->permissions = $this->cache->remember($this->cacheKey, config('permission.cache_expiration_time'), function () {
return app(Permission::class)->with('roles')->get();
});
}
return $this->permissions;
}
PermissionRegistrar is also being created every time, it should be registered as a singleton
$this->app->singleton(PermissionRegistrar::class);
Sorry for just throwing it here but I dont have the time for a PR right now
Some additional data from Xdebug
(times in ms; calls ordered by own time; total request time at the bottom of the images)
The code as it is

With memoization of PermissionRegistrar::getPermissions

With memoization of PermissionRegistrar::getPermissions + memoization of HasPermissions::hasPermissionTo

I am using file cache store, but I am pretty sure this problem exists with all other cache stores except array
Thanks for posting the performance stats. Clearly those results are compelling.
I'd be interested in the larger memory footprint introduced by the changes. Granted, the overall impact is probably limited, since the data is always being passed around at some point, just not retained for as long.
PR https://github.com/spatie/laravel-permission/pull/732/files adds more granular memoizing at the per-permission-combination level, but has the caveat of requiring a cache system that supports tagging in order to make cache-flushing possible. Looking for a way to remove that dependency.
I imagine there's a happy hybrid somewhere in these discussions ...
Memory wise, I actually saw a decrease from 25mb to 11mb at this page I tested (reported by Laravel Debugbar).
A solution I found to this 'unserialize every time' problem, without any change to PermissionRegistrar, was to make a custom cache store that uses both array store and the default cache store (file in my case), the array store acts basically as a cache to the cache :P
Good points. I hope to look at both of these more over the weekend.
Your custom cache-of-the-cache is a fun workaround. :D
@williamoliveira I am facing exact same problem. I have more than 200 permissions and SaaS solution. with only 50+ users, my permission_role table has already crossed 4000 entries. I applied your solution but it didn't reduce the memory usage ( at the moment more than 70mb :( )
I was able to update the code you mentioned in the "vendor >> spatie >> laravel-permission >> src >> PermissionRegistrar.php" file.
However, what file I need to modify to register permissionregistrar class as singleton? sorry, basically I didn't understand this line.
registered as a singleton
$this->app->singleton(PermissionRegistrar::class);
Any help would be appreciated. My page load should not take more than 2 seconds, but now it takes on an average 10 sec.
@mailnike you just place it in the register method of a Service Provider, AppServiceProvider will do
@williamoliveira thanks for the quick reply. I updated 'AppServiceProvider' > 'register' method with '$this->app->singleton(PermissionRegistrar::class);' ... however, it doesn't seem to have much difference. question, do I need to resync these permissions? or some step I might be missing?
Yes we need to find a way that the permissions and roles are cached totally and only set invalid in case of change on user level. The Singleton is a good start which I support to lower the serialize issue. In worst case, yes we need a cache per call and a over all cache.
The current reading of all permissions from DB is only effective for a small list of permissions or when there are stored across sessions within the mem. I believe we need to give a new config parameter to define if totally mem-cache will be used, all permissions will be read each time from db (current default behavior) or if only a subset of permissions will be loaded (as before #710/#550).
What do you think? We need to set ourself the goal to manage over 2000 permissions without any big effort. This is a realistic number in todays SaaS / Multiple Tenant System.
Why this library don't use json fields for cache roles and permissions?
Or even for remove pivot table so it have roles and permissions table and then those models that need to use roles and permissions like users add field roles_json and permissions_json
Most helpful comment
Yes we need to find a way that the permissions and roles are cached totally and only set invalid in case of change on user level. The Singleton is a good start which I support to lower the serialize issue. In worst case, yes we need a cache per call and a over all cache.
The current reading of all permissions from DB is only effective for a small list of permissions or when there are stored across sessions within the mem. I believe we need to give a new config parameter to define if totally mem-cache will be used, all permissions will be read each time from db (current default behavior) or if only a subset of permissions will be loaded (as before #710/#550).
What do you think? We need to set ourself the goal to manage over 2000 permissions without any big effort. This is a realistic number in todays SaaS / Multiple Tenant System.