├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── policy-soft-cache.php └── src ├── Contracts └── SoftCacheable.php ├── LaravelPolicySoftCache.php └── LaravelPolicySoftCacheServiceProvider.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-policy-soft-cache` will be documented in this file. 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) innoge 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Policy Soft Cache Package 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/innoge/laravel-policy-soft-cache.svg?style=flat-square)](https://packagist.org/packages/innoge/laravel-policy-soft-cache) 4 | [![Tests](https://github.com/InnoGE/laravel-policy-soft-cache/actions/workflows/run-tests.yml/badge.svg)](https://github.com/InnoGE/laravel-policy-soft-cache/actions/workflows/run-tests.yml) 5 | [![Fix PHP code style issues](https://github.com/InnoGE/laravel-policy-soft-cache/actions/workflows/fix-php-code-style-issues.yml/badge.svg)](https://github.com/InnoGE/laravel-policy-soft-cache/actions/workflows/fix-php-code-style-issues.yml) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/innoge/laravel-policy-soft-cache.svg?style=flat-square)](https://packagist.org/packages/innoge/laravel-policy-soft-cache) 7 | 8 | Optimize your Laravel application's performance with soft caching for policy checks. This package caches policy invocations to prevent redundant checks within the same request lifecycle, enhancing your application's response times. 9 | 10 | ## Requirements 11 | 12 | This package is compatible with ```Laravel 9, 10, 11, 12```, and PHP >= 8.1. 13 | ## Installation 14 | 15 | You can install the package via composer: 16 | 17 | ```bash 18 | composer require innoge/laravel-policy-soft-cache 19 | ``` 20 | 21 | You can publish the config file with: 22 | 23 | ```bash 24 | php artisan vendor:publish --provider="Innoge\LaravelPolicySoftCache\LaravelPolicySoftCacheServiceProvider" 25 | ``` 26 | 27 | This is the contents of the published config file: 28 | 29 | ```php 30 | return [ 31 | /* 32 | * When enabled, the package will cache the results of all Policies in your Laravel application 33 | */ 34 | 'cache_all_policies' => env('CACHE_ALL_POLICIES', true), 35 | ]; 36 | ``` 37 | 38 | You can also use `CACHE_ALL_POLICIES` in your `.env` file to change it. 39 | ```.dotenv 40 | CACHE_ALL_POLICIES=false 41 | ``` 42 | 43 | ## Usage 44 | 45 | By default, this package caches all policy calls of your entire application. You can disable this behavior by setting the ```cache_all_policies```configuration to false. Now you can specify which Policy classes should be soft cached and which not. If you want your policy to be cached, add the ```Innoge\LaravelPolicySoftCache\Contracts\SoftCacheable``` interface. 46 | 47 | For Example: 48 | 49 | ``` 50 | use Innoge\LaravelPolicySoftCache\Contracts\SoftCacheable; 51 | 52 | class UserPolicy implements SoftCacheable 53 | { 54 | ... 55 | } 56 | ``` 57 | 58 | ## Clearing the cache 59 | Sometimes you want to clear the policy cache after model changes. You can call the ```Innoge\LaravelPolicySoftCache::flushCache();``` method. 60 | 61 | ## Known Issues 62 | ### Gate::before and Service Provider Load Order 63 | 64 | When the `innoge/laravel-policy-soft-cache` package is installed in an application that utilizes `Gate::before`, typically defined in the `AuthServiceProvider`, a conflict may arise due to the order in which service providers are loaded. 65 | 66 | #### Resolution Steps 67 | To resolve this issue, follow these steps: 68 | 69 | 1. **Manual Service Provider Registration**: Add `\Innoge\LaravelPolicySoftCache\LaravelPolicySoftCacheServiceProvider::class` to the end of the `providers` array in your `config/app.php`. This manual registration ensures that the `LaravelPolicySoftCacheServiceProvider` loads after all other service providers, including `AuthServiceProvider`. 70 | 71 | ```php 72 | 'providers' => [ 73 | // Other Service Providers 74 | 75 | \Innoge\LaravelPolicySoftCache\LaravelPolicySoftCacheServiceProvider::class, 76 | ], 77 | ``` 78 | 79 | 2. **Disable Auto-Discovery for the Package**: To prevent Laravel's auto-discovery mechanism from automatically loading the service provider, add `innoge/laravel-policy-soft-cache` to the `dont-discover` array in your `composer.json`. This step is crucial for maintaining the manual load order. 80 | 81 | ```json 82 | "extra": { 83 | "laravel": { 84 | "dont-discover": ["innoge/laravel-policy-soft-cache"] 85 | } 86 | }, 87 | ``` 88 | 89 | 3. **Reinstall Dependencies**: After updating your `composer.json`, run `composer install` to apply the changes. This step is necessary for the changes to take effect. 90 | 91 | ```bash 92 | composer install 93 | ``` 94 | 95 | 96 | ## Testing 97 | 98 | ```bash 99 | composer test 100 | ``` 101 | 102 | ## Changelog 103 | 104 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 105 | 106 | ## Contributing 107 | 108 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 109 | 110 | ## Security Vulnerabilities 111 | 112 | Please review [our security policy](../../security/policy) on how to report security vulnerabilities. 113 | 114 | ## Credits 115 | 116 | - [Tim Geisendörfer](https://github.com/geisi) 117 | - [All Contributors](../../contributors) 118 | 119 | ## License 120 | 121 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 122 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "innoge/laravel-policy-soft-cache", 3 | "description": "This package helps prevent performance problems with frequent Policy calls within your application lifecycle.", 4 | "keywords": [ 5 | "innoge", 6 | "laravel", 7 | "laravel-policy-soft-cache" 8 | ], 9 | "homepage": "https://github.com/innoge/laravel-policy-soft-cache", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Tim Geisendörfer", 14 | "email": "geisi@users.noreply.github.com", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.1", 20 | "spatie/laravel-package-tools": "^1.13.0", 21 | "illuminate/contracts": "^9.0|^10.0|^11.0|^12.0" 22 | }, 23 | "require-dev": { 24 | "laravel/pint": "^1.0", 25 | "nunomaduro/larastan": "^2.0.1", 26 | "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", 27 | "pestphp/pest": "^1.0|^2.0|^3.0", 28 | "pestphp/pest-plugin-laravel": "^1.0|^2.0|^3.0" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "Innoge\\LaravelPolicySoftCache\\": "src", 33 | "Innoge\\LaravelPolicySoftCache\\Database\\Factories\\": "database/factories" 34 | } 35 | }, 36 | "autoload-dev": { 37 | "psr-4": { 38 | "Innoge\\LaravelPolicySoftCache\\Tests\\": "tests" 39 | } 40 | }, 41 | "scripts": { 42 | "post-autoload-dump": "@php ./vendor/bin/testbench package:discover --ansi", 43 | "analyse": "vendor/bin/phpstan analyse", 44 | "test": "vendor/bin/pest", 45 | "test-coverage": "vendor/bin/pest --coverage", 46 | "format": "vendor/bin/pint" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true, 52 | "phpstan/extension-installer": true 53 | } 54 | }, 55 | "extra": { 56 | "laravel": { 57 | "providers": [ 58 | "Innoge\\LaravelPolicySoftCache\\LaravelPolicySoftCacheServiceProvider" 59 | ], 60 | "aliases": { 61 | "LaravelPolicySoftCache": "Innoge\\LaravelPolicySoftCache\\Facades\\LaravelPolicySoftCache" 62 | } 63 | } 64 | }, 65 | "minimum-stability": "dev", 66 | "prefer-stable": true 67 | } 68 | -------------------------------------------------------------------------------- /config/policy-soft-cache.php: -------------------------------------------------------------------------------- 1 | env('CACHE_ALL_POLICIES', true), 8 | ]; 9 | -------------------------------------------------------------------------------- /src/Contracts/SoftCacheable.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | protected array $cache = []; 16 | 17 | /** 18 | * @throws BindingResolutionException 19 | */ 20 | public static function flushCache(): void 21 | { 22 | app()->make(static::class)->cache = []; 23 | } 24 | 25 | public function handleGateCall(mixed $user, string $ability, mixed $args): mixed 26 | { 27 | if (! is_array($args)) { 28 | $args = [$args]; 29 | } 30 | 31 | if (! ($user instanceof Model)) { 32 | return null; 33 | } 34 | 35 | $model = $args[0] ?? null; 36 | 37 | $policy = Gate::getPolicyFor($model); 38 | 39 | if ($model && $this->shouldCache($policy, $ability)) { 40 | return $this->callPolicyMethod($user, $policy, $ability, $args); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | protected function shouldCache(?object $policy, string $ability): bool 47 | { 48 | // when policy is not filled don't cache it 49 | if (blank($policy)) { 50 | return false; 51 | } 52 | 53 | // when policy is not an object don't cache it 54 | if (! is_object($policy)) { 55 | return false; 56 | } 57 | 58 | // when policy doesn't have the ability don't cache it 59 | if (! method_exists($policy, $ability)) { 60 | return false; 61 | } 62 | 63 | // when policy is soft cacheable cache it 64 | if ($policy instanceof SoftCacheable) { 65 | return true; 66 | } 67 | 68 | // when config is set to cache all policies cache it 69 | return config('policy-soft-cache.cache_all_policies', false) === true; 70 | } 71 | 72 | /** 73 | * @param array $args 74 | */ 75 | protected function callPolicyMethod(Model $user, object $policy, string $ability, array $args): mixed 76 | { 77 | $cacheKey = $this->getCacheKey($user, $policy, $args, $ability); 78 | 79 | if (isset($this->cache[$cacheKey])) { 80 | return $this->cache[$cacheKey]; 81 | } 82 | 83 | // If this first argument is a string, that means they are passing a class name to 84 | // the policy, so we remove it because it shouldn't be in the method as parameter. 85 | if (isset($args[0]) && is_string($args[0])) { 86 | array_shift($args); 87 | } 88 | 89 | $result = $policy->{$ability}(...array_merge([$user], $args)); 90 | $this->cache[$cacheKey] = $result; 91 | 92 | return $result; 93 | } 94 | 95 | /** 96 | * @param array $args 97 | */ 98 | protected function getCacheKey(Model $user, object $policy, array $args, string $ability): string 99 | { 100 | return collect([ 101 | //the user class 102 | get_class($user), 103 | //the user id 104 | $user->{$user->getKeyName()}, 105 | //hash of the arguments 106 | hash_hmac('sha512', json_encode($args), config('app.key')), 107 | $ability, 108 | get_class($policy), 109 | ])->join('_'); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/LaravelPolicySoftCacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-policy-soft-cache') 21 | ->hasConfigFile('policy-soft-cache'); 22 | } 23 | 24 | public function boot(): void 25 | { 26 | $this->app->singleton(LaravelPolicySoftCache::class, function () { 27 | return new LaravelPolicySoftCache(); 28 | }); 29 | 30 | $this->publishes([ 31 | __DIR__.'/../config/policy-soft-cache.php' => config_path('policy-soft-cache.php'), 32 | ]); 33 | 34 | /* 35 | * Flush Cache on every application boot 36 | */ 37 | LaravelPolicySoftCache::flushCache(); 38 | 39 | Gate::before(function (Model $user, string $ability, array $args) { 40 | return $this->app->make(LaravelPolicySoftCache::class) 41 | ->handleGateCall($user, $ability, $args); 42 | }); 43 | } 44 | } 45 | --------------------------------------------------------------------------------