├── src ├── Events │ ├── ClearedResponseCache.php │ ├── ClearingResponseCache.php │ ├── ClearingResponseCacheFailed.php │ ├── CacheMissed.php │ └── ResponseCacheHit.php ├── Hasher │ ├── RequestHasher.php │ └── DefaultHasher.php ├── Serializers │ ├── Serializer.php │ └── DefaultSerializer.php ├── Exceptions │ └── CouldNotUnserialize.php ├── Middlewares │ ├── DoNotCacheResponse.php │ └── CacheResponse.php ├── Replacers │ ├── Replacer.php │ └── CsrfTokenReplacer.php ├── Commands │ └── ClearCommand.php ├── CacheProfiles │ ├── CacheProfile.php │ ├── BaseCacheProfile.php │ └── CacheAllSuccessfulGetRequests.php ├── Facades │ └── ResponseCache.php ├── CacheItemSelector │ ├── CacheItemSelector.php │ └── AbstractRequestBuilder.php ├── ResponseCacheServiceProvider.php ├── ResponseCacheRepository.php └── ResponseCache.php ├── UPGRADING.md ├── LICENSE.md ├── .php-cs-fixer.php ├── composer.json ├── config └── responsecache.php ├── CHANGELOG.md └── README.md /src/Events/ClearedResponseCache.php: -------------------------------------------------------------------------------- 1 | attributes->add(['responsecache.doNotCache' => true]); 13 | 14 | return $next($request); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Replacers/Replacer.php: -------------------------------------------------------------------------------- 1 | clear(); 17 | 18 | $this->info('Response cache cleared!'); 19 | } 20 | 21 | protected function clear() 22 | { 23 | if ($url = $this->option('url')) { 24 | return ResponseCache::forget($url); 25 | } 26 | 27 | ResponseCache::clear(); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/CacheProfiles/CacheProfile.php: -------------------------------------------------------------------------------- 1 | addSeconds( 20 | config('responsecache.cache_lifetime_in_seconds') 21 | ); 22 | } 23 | 24 | public function useCacheNameSuffix(Request $request): string 25 | { 26 | return Auth::check() 27 | ? (string) Auth::id() 28 | : ''; 29 | } 30 | 31 | public function isRunningInConsole(): bool 32 | { 33 | if (app()->environment('testing')) { 34 | return false; 35 | } 36 | 37 | return app()->runningInConsole(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading 2 | 3 | Because there are many breaking changes an upgrade is not that easy. There are many edge cases this guide does not cover. We accept PRs to improve this guide. 4 | 5 | ## 6.0.0 6 | 7 | If you're using the default settings you can upgrade without any problems. 8 | 9 | - By default the `CsrfTokenReplacer` will be applied before caching the request. For most users, this will be harmless 10 | - The `flush` method has been removed, use `clear` instead 11 | - The `Flush` command has been removed. Use `ClearCommand` instead 12 | 13 | 14 | ## 5.0.0 15 | 16 | As of Laravel 5.8 defining cache time in seconds is supported. All mentions of response cache time should be changed from minutes to seconds: 17 | 18 | - If you've published to config file, change `cache_lifetime_in_minutes` to `cache_lifetime_in_seconds` 19 | - If you're using the `cacheResponse` middleware, change the optional time parameter to seconds (value * 60) 20 | - If you're extending `CacheResponse`, `ResponseCacheRepository`, `BaseCacheProfile` or `CacheResponse` you should check the relevant methods 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Spatie bvba 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 | -------------------------------------------------------------------------------- /src/Replacers/CsrfTokenReplacer.php: -------------------------------------------------------------------------------- 1 | '; 10 | 11 | public function prepareResponseToCache(Response $response): void 12 | { 13 | $csrf_token = csrf_token(); 14 | $content = $response->getContent(); 15 | 16 | if (! $content || ! $csrf_token) { 17 | return; 18 | } 19 | 20 | $response->setContent(str_replace( 21 | $csrf_token, 22 | $this->replacementString, 23 | $content, 24 | )); 25 | } 26 | 27 | public function replaceInCachedResponse(Response $response): void 28 | { 29 | $csrf_token = csrf_token(); 30 | $content = $response->getContent(); 31 | 32 | if (! $content || ! $csrf_token) { 33 | return; 34 | } 35 | 36 | $response->setContent(str_replace( 37 | $this->replacementString, 38 | $csrf_token, 39 | $content, 40 | )); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Facades/ResponseCache.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->notName('*.blade.php') 10 | ->ignoreDotFiles(true) 11 | ->ignoreVCS(true); 12 | 13 | return (new PhpCsFixer\Config()) 14 | ->setRules([ 15 | '@PSR12' => true, 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 18 | 'no_unused_imports' => true, 19 | 'not_operator_with_successor_space' => true, 20 | 'trailing_comma_in_multiline' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'method_argument_space' => [ 30 | 'on_multiline' => 'ensure_fully_multiline', 31 | 'keep_multiple_spaces_after_comma' => true, 32 | ], 33 | 'single_trait_insert_per_statement' => true, 34 | ]) 35 | ->setFinder($finder); 36 | -------------------------------------------------------------------------------- /src/Hasher/DefaultHasher.php: -------------------------------------------------------------------------------- 1 | getCacheNameSuffix($request); 19 | 20 | return 'responsecache-' . hash( 21 | 'xxh128', 22 | "{$request->getHost()}-{$this->getNormalizedRequestUri($request)}-{$request->getMethod()}/$cacheNameSuffix" 23 | ); 24 | } 25 | 26 | protected function getNormalizedRequestUri(Request $request): string 27 | { 28 | if ($queryString = $request->getQueryString()) { 29 | $queryString = '?'.$queryString; 30 | } 31 | 32 | return $request->getBaseUrl().$request->getPathInfo().$queryString; 33 | } 34 | 35 | protected function getCacheNameSuffix(Request $request) 36 | { 37 | if ($request->attributes->has('responsecache.cacheNameSuffix')) { 38 | return $request->attributes->get('responsecache.cacheNameSuffix'); 39 | } 40 | 41 | return $this->cacheProfile->useCacheNameSuffix($request); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/CacheItemSelector/CacheItemSelector.php: -------------------------------------------------------------------------------- 1 | tags = is_array($tags) ? $tags : func_get_args(); 23 | 24 | return $this; 25 | } 26 | 27 | public function forUrls(string | array $urls): static 28 | { 29 | $this->urls = is_array($urls) ? $urls : func_get_args(); 30 | 31 | return $this; 32 | } 33 | 34 | public function forget(): void 35 | { 36 | collect($this->urls) 37 | ->map(function ($uri) { 38 | $request = $this->build($uri); 39 | 40 | return $this->hasher->getHashFor($request); 41 | }) 42 | ->filter(fn ($hash) => $this->taggedCache($this->tags)->has($hash)) 43 | ->each(fn ($hash) => $this->taggedCache($this->tags)->forget($hash)); 44 | } 45 | 46 | protected function taggedCache(array $tags = []): ResponseCacheRepository 47 | { 48 | return empty($tags) 49 | ? $this->cache 50 | : $this->cache->tags($tags); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/CacheProfiles/CacheAllSuccessfulGetRequests.php: -------------------------------------------------------------------------------- 1 | ajax()) { 14 | return false; 15 | } 16 | 17 | if ($this->isRunningInConsole()) { 18 | return false; 19 | } 20 | 21 | return $request->isMethod('get'); 22 | } 23 | 24 | public function shouldCacheResponse(Response $response): bool 25 | { 26 | if (! $this->hasCacheableResponseCode($response)) { 27 | return false; 28 | } 29 | 30 | if (! $this->hasCacheableContentType($response)) { 31 | return false; 32 | } 33 | 34 | return true; 35 | } 36 | 37 | public function hasCacheableResponseCode(Response $response): bool 38 | { 39 | if ($response->isSuccessful()) { 40 | return true; 41 | } 42 | 43 | if ($response->isRedirection()) { 44 | return true; 45 | } 46 | 47 | return false; 48 | } 49 | 50 | public function hasCacheableContentType(Response $response): bool 51 | { 52 | $contentType = $response->headers->get('Content-Type', ''); 53 | 54 | if (str_starts_with($contentType, 'text/')) { 55 | return true; 56 | } 57 | 58 | if (Str::contains($contentType, ['/json', '+json'])) { 59 | return true; 60 | } 61 | 62 | return false; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/ResponseCacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | name('laravel-responsecache') 20 | ->hasConfigFile() 21 | ->hasCommands([ 22 | ClearCommand::class, 23 | ]); 24 | } 25 | 26 | public function packageBooted() 27 | { 28 | $this->app->bind(CacheProfile::class, function (Container $app) { 29 | return $app->make(config('responsecache.cache_profile')); 30 | }); 31 | 32 | $this->app->bind(RequestHasher::class, function (Container $app) { 33 | return $app->make(config('responsecache.hasher')); 34 | }); 35 | 36 | $this->app->bind(Serializer::class, function (Container $app) { 37 | return $app->make(config('responsecache.serializer')); 38 | }); 39 | 40 | $this->app->when(ResponseCacheRepository::class) 41 | ->needs(Repository::class) 42 | ->give(function (): Repository { 43 | $repository = app('cache')->store(config('responsecache.cache_store')); 44 | if (! empty(config('responsecache.cache_tag'))) { 45 | return $repository->tags(config('responsecache.cache_tag')); 46 | } 47 | 48 | return $repository; 49 | }); 50 | 51 | $this->app->singleton('responsecache', ResponseCache::class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "spatie/laravel-responsecache", 3 | "description": "Speed up a Laravel application by caching the entire response", 4 | "keywords": [ 5 | "spatie", 6 | "laravel-responsecache", 7 | "laravel", 8 | "cache", 9 | "response", 10 | "performance" 11 | ], 12 | "homepage": "https://github.com/spatie/laravel-responsecache", 13 | "license": "MIT", 14 | "authors": [ 15 | { 16 | "name": "Freek Van der Herten", 17 | "email": "freek@spatie.be", 18 | "homepage": "https://spatie.be", 19 | "role": "Developer" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.2", 24 | "illuminate/cache": "^10.0|^11.0|^12.0", 25 | "illuminate/container": "^10.0|^11.0|^12.0", 26 | "illuminate/console": "^10.0|^11.0|^12.0", 27 | "illuminate/http": "^10.0|^11.0|^12.0", 28 | "illuminate/support": "^10.0|^11.0|^12.0", 29 | "nesbot/carbon": "^2.63|^3.0", 30 | "spatie/laravel-package-tools": "^1.9" 31 | }, 32 | "require-dev": { 33 | "laravel/framework": "^10.0|^11.0|^12.0", 34 | "mockery/mockery": "^1.4", 35 | "orchestra/testbench": "^8.0|^9.0|^10.0", 36 | "pestphp/pest": "^2.22|^3.0" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Spatie\\ResponseCache\\": "src" 41 | } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Spatie\\ResponseCache\\Test\\": "tests" 46 | } 47 | }, 48 | "scripts": { 49 | "test": "vendor/bin/pest" 50 | }, 51 | "config": { 52 | "sort-packages": true, 53 | "allow-plugins": { 54 | "pestphp/pest-plugin": true 55 | } 56 | }, 57 | "extra": { 58 | "laravel": { 59 | "providers": [ 60 | "Spatie\\ResponseCache\\ResponseCacheServiceProvider" 61 | ], 62 | "aliases": { 63 | "ResponseCache": "Spatie\\ResponseCache\\Facades\\ResponseCache" 64 | } 65 | } 66 | }, 67 | "minimum-stability": "dev", 68 | "prefer-stable": true 69 | } 70 | -------------------------------------------------------------------------------- /src/ResponseCacheRepository.php: -------------------------------------------------------------------------------- 1 | cache->put($key, $this->responseSerializer->serialize($response), is_numeric($seconds) ? now()->addSeconds($seconds) : $seconds); 22 | } 23 | 24 | public function has(string $key): bool 25 | { 26 | return $this->cache->has($key); 27 | } 28 | 29 | public function get(string $key): Response 30 | { 31 | return $this->responseSerializer->unserialize($this->cache->get($key) ?? ''); 32 | } 33 | 34 | /** 35 | * If the response cache tag is empty, or a Store doesn't support tags, the whole cache will be cleared. 36 | 37 | * @return bool Whether the cache was cleared successfully. 38 | */ 39 | public function clear(): bool 40 | { 41 | if ($this->isTagged($this->cache)) { 42 | return $this->cache->flush(); 43 | } 44 | 45 | if (empty(config('responsecache.cache_tag'))) { 46 | return $this->cache->clear(); 47 | } 48 | 49 | return $this->cache->tags(config('responsecache.cache_tag'))->flush(); 50 | } 51 | 52 | public function forget(string $key): bool 53 | { 54 | return $this->cache->forget($key); 55 | } 56 | 57 | public function tags(array $tags): self 58 | { 59 | if ($this->isTagged($this->cache)) { 60 | $tags = array_merge($this->cache->getTags()->getNames(), $tags); 61 | } 62 | 63 | return new self($this->responseSerializer, $this->cache->tags($tags)); 64 | } 65 | 66 | public function isTagged($repository): bool 67 | { 68 | return $repository instanceof TaggedCache && ! empty($repository->getTags()); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Serializers/DefaultSerializer.php: -------------------------------------------------------------------------------- 1 | getResponseData($response)); 18 | } 19 | 20 | public function unserialize(string $serializedResponse): Response 21 | { 22 | $responseProperties = unserialize($serializedResponse); 23 | 24 | if (! $this->containsValidResponseProperties($responseProperties)) { 25 | throw CouldNotUnserialize::serializedResponse($serializedResponse); 26 | } 27 | 28 | $response = $this->buildResponse($responseProperties); 29 | 30 | $response->headers = $responseProperties['headers']; 31 | 32 | return $response; 33 | } 34 | 35 | protected function getResponseData(Response $response): array 36 | { 37 | $statusCode = $response->getStatusCode(); 38 | $headers = $response->headers; 39 | 40 | if ($response instanceof BinaryFileResponse) { 41 | $content = $response->getFile()->getPathname(); 42 | $type = static::RESPONSE_TYPE_FILE; 43 | 44 | return compact('statusCode', 'headers', 'content', 'type'); 45 | } 46 | 47 | $content = $response->getContent(); 48 | $type = static::RESPONSE_TYPE_NORMAL; 49 | 50 | return compact('statusCode', 'headers', 'content', 'type'); 51 | } 52 | 53 | protected function containsValidResponseProperties($properties): bool 54 | { 55 | if (! is_array($properties)) { 56 | return false; 57 | } 58 | 59 | if (! isset($properties['content'], $properties['statusCode'])) { 60 | return false; 61 | } 62 | 63 | return true; 64 | } 65 | 66 | protected function buildResponse(array $responseProperties): Response 67 | { 68 | $type = $responseProperties['type'] ?? static::RESPONSE_TYPE_NORMAL; 69 | 70 | if ($type === static::RESPONSE_TYPE_FILE) { 71 | return new BinaryFileResponse( 72 | $responseProperties['content'], 73 | $responseProperties['statusCode'] 74 | ); 75 | } 76 | 77 | return new IlluminateResponse($responseProperties['content'], $responseProperties['statusCode']); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/CacheItemSelector/AbstractRequestBuilder.php: -------------------------------------------------------------------------------- 1 | method = 'PUT'; 19 | 20 | return $this; 21 | } 22 | 23 | public function withPatchMethod(): self 24 | { 25 | $this->method = 'PATCH'; 26 | 27 | return $this; 28 | } 29 | 30 | public function withPostMethod(): self 31 | { 32 | $this->method = 'POST'; 33 | 34 | return $this; 35 | } 36 | 37 | /** 38 | * if method is GET then will be converted to query 39 | * otherwise it will became part of request input 40 | */ 41 | public function withParameters(array $parameters): self 42 | { 43 | $this->parameters = $parameters; 44 | 45 | return $this; 46 | } 47 | 48 | public function withCookies(array $cookies): self 49 | { 50 | $this->cookies = $cookies; 51 | 52 | return $this; 53 | } 54 | 55 | public function withHeaders(array $headers): self 56 | { 57 | $this->server = collect($this->server) 58 | ->filter(fn (string $val, string $key) => ! str_starts_with($key, 'HTTP_')) 59 | ->merge(collect($headers) 60 | ->mapWithKeys(function (string $val, string $key) { 61 | return ['HTTP_' . str_replace('-', '_', Str::upper($key)) => $val]; 62 | })) 63 | ->toArray(); 64 | 65 | return $this; 66 | } 67 | 68 | public function withRemoteAddress($remoteAddress): self 69 | { 70 | $this->server['REMOTE_ADDR'] = $remoteAddress; 71 | 72 | return $this; 73 | } 74 | 75 | public function usingSuffix($cacheNameSuffix): self 76 | { 77 | $this->cacheNameSuffix = $cacheNameSuffix; 78 | 79 | return $this; 80 | } 81 | 82 | protected function build(string $uri): Request 83 | { 84 | $request = Request::create( 85 | url($uri), 86 | $this->method, 87 | $this->parameters, 88 | $this->cookies, 89 | [], 90 | $this->server 91 | ); 92 | 93 | if (isset($this->cacheNameSuffix)) { 94 | $request->attributes->add([ 95 | 'responsecache.cacheNameSuffix' => $this->cacheNameSuffix, 96 | ]); 97 | } 98 | 99 | return $request; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /config/responsecache.php: -------------------------------------------------------------------------------- 1 | env('RESPONSE_CACHE_ENABLED', true), 8 | 9 | /* 10 | * The given class will determinate if a request should be cached. The 11 | * default class will cache all successful GET-requests. 12 | * 13 | * You can provide your own class given that it implements the 14 | * CacheProfile interface. 15 | */ 16 | 'cache_profile' => Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests::class, 17 | 18 | /* 19 | * Optionally, you can specify a header that will force a cache bypass. 20 | * This can be useful to monitor the performance of your application. 21 | */ 22 | 'cache_bypass_header' => [ 23 | 'name' => env('CACHE_BYPASS_HEADER_NAME', null), 24 | 'value' => env('CACHE_BYPASS_HEADER_VALUE', null), 25 | ], 26 | 27 | /* 28 | * When using the default CacheRequestFilter this setting controls the 29 | * default number of seconds responses must be cached. 30 | */ 31 | 'cache_lifetime_in_seconds' => (int) env('RESPONSE_CACHE_LIFETIME', 60 * 60 * 24 * 7), 32 | 33 | /* 34 | * This setting determines if a http header named with the cache time 35 | * should be added to a cached response. This can be handy when 36 | * debugging. 37 | */ 38 | 'add_cache_time_header' => env('APP_DEBUG', false), 39 | 40 | /* 41 | * This setting determines the name of the http header that contains 42 | * the time at which the response was cached 43 | */ 44 | 'cache_time_header_name' => env('RESPONSE_CACHE_HEADER_NAME', 'laravel-responsecache'), 45 | 46 | /* 47 | * This setting determines if a http header named with the cache age 48 | * should be added to a cached response. This can be handy when 49 | * debugging. 50 | * ONLY works when "add_cache_time_header" is also active! 51 | */ 52 | 'add_cache_age_header' => env('RESPONSE_CACHE_AGE_HEADER', false), 53 | 54 | /* 55 | * This setting determines the name of the http header that contains 56 | * the age of cache 57 | */ 58 | 'cache_age_header_name' => env('RESPONSE_CACHE_AGE_HEADER_NAME', 'laravel-responsecache-age'), 59 | 60 | /* 61 | * Here you may define the cache store that should be used to store 62 | * requests. This can be the name of any store that is 63 | * configured in app/config/cache.php 64 | */ 65 | 'cache_store' => env('RESPONSE_CACHE_DRIVER', 'file'), 66 | 67 | /* 68 | * Here you may define replacers that dynamically replace content from the response. 69 | * Each replacer must implement the Replacer interface. 70 | */ 71 | 'replacers' => [ 72 | \Spatie\ResponseCache\Replacers\CsrfTokenReplacer::class, 73 | ], 74 | 75 | /* 76 | * If the cache driver you configured supports tags, you may specify a tag name 77 | * here. All responses will be tagged. When clearing the responsecache only 78 | * items with that tag will be flushed. 79 | * 80 | * You may use a string or an array here. 81 | */ 82 | 'cache_tag' => '', 83 | 84 | /* 85 | * This class is responsible for generating a hash for a request. This hash 86 | * is used to look up a cached response. 87 | */ 88 | 'hasher' => \Spatie\ResponseCache\Hasher\DefaultHasher::class, 89 | 90 | /* 91 | * This class is responsible for serializing responses. 92 | */ 93 | 'serializer' => \Spatie\ResponseCache\Serializers\DefaultSerializer::class, 94 | ]; 95 | -------------------------------------------------------------------------------- /src/ResponseCache.php: -------------------------------------------------------------------------------- 1 | cacheProfile->enabled($request); 28 | } 29 | 30 | public function shouldCache(Request $request, Response $response): bool 31 | { 32 | if ($request->attributes->has('responsecache.doNotCache')) { 33 | return false; 34 | } 35 | 36 | if (! $this->cacheProfile->shouldCacheRequest($request)) { 37 | return false; 38 | } 39 | 40 | return $this->cacheProfile->shouldCacheResponse($response); 41 | } 42 | 43 | public function shouldBypass(Request $request): bool 44 | { 45 | // Ensure we return if cache_bypass_header is not setup 46 | if (! config('responsecache.cache_bypass_header.name')) { 47 | return false; 48 | } 49 | // Ensure we return if cache_bypass_header is not setup 50 | if (! config('responsecache.cache_bypass_header.value')) { 51 | return false; 52 | } 53 | 54 | return $request->header(config('responsecache.cache_bypass_header.name')) === (string) config('responsecache.cache_bypass_header.value'); 55 | } 56 | 57 | public function cacheResponse( 58 | Request $request, 59 | Response $response, 60 | ?int $lifetimeInSeconds = null, 61 | array $tags = [] 62 | ): Response { 63 | if (config('responsecache.add_cache_time_header')) { 64 | $response = $this->addCachedHeader($response); 65 | } 66 | 67 | $this->taggedCache($tags)->put( 68 | $this->hasher->getHashFor($request), 69 | $response, 70 | $lifetimeInSeconds ?? $this->cacheProfile->cacheRequestUntil($request), 71 | ); 72 | 73 | return $response; 74 | } 75 | 76 | public function hasBeenCached(Request $request, array $tags = []): bool 77 | { 78 | return config('responsecache.enabled') 79 | ? $this->taggedCache($tags)->has($this->hasher->getHashFor($request)) 80 | : false; 81 | } 82 | 83 | public function getCachedResponseFor(Request $request, array $tags = []): Response 84 | { 85 | return $this->taggedCache($tags)->get($this->hasher->getHashFor($request)); 86 | } 87 | 88 | public function clear(array $tags = []): bool 89 | { 90 | event(new ClearingResponseCache()); 91 | 92 | $result = $this->taggedCache($tags)->clear(); 93 | 94 | $resultEvent = $result 95 | ? new ClearedResponseCache() 96 | : new ClearingResponseCacheFailed(); 97 | 98 | event($resultEvent); 99 | 100 | return $result; 101 | } 102 | 103 | protected function addCachedHeader(Response $response): Response 104 | { 105 | $clonedResponse = clone $response; 106 | 107 | $clonedResponse->headers->set( 108 | config('responsecache.cache_time_header_name'), 109 | Carbon::now()->toRfc2822String(), 110 | ); 111 | 112 | return $clonedResponse; 113 | } 114 | 115 | /** 116 | * @param string|array $uris 117 | * @param string[] $tags 118 | * 119 | * @return \Spatie\ResponseCache\ResponseCache 120 | */ 121 | public function forget(string | array $uris, array $tags = []): self 122 | { 123 | event(new ClearingResponseCache()); 124 | 125 | $uris = is_array($uris) ? $uris : func_get_args(); 126 | $this->selectCachedItems()->forUrls($uris)->forget(); 127 | 128 | event(new ClearedResponseCache()); 129 | 130 | return $this; 131 | } 132 | 133 | public function selectCachedItems(): CacheItemSelector 134 | { 135 | return new CacheItemSelector($this->hasher, $this->cache); 136 | } 137 | 138 | protected function taggedCache(array $tags = []): ResponseCacheRepository 139 | { 140 | if (empty($tags)) { 141 | return $this->cache; 142 | } 143 | 144 | return $this->cache->tags($tags); 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/Middlewares/CacheResponse.php: -------------------------------------------------------------------------------- 1 | responseCache = $responseCache; 24 | } 25 | 26 | public static function using($lifetime, ...$tags): string 27 | { 28 | return static::class.':'.implode(',', [$lifetime, ...$tags]); 29 | } 30 | 31 | public function handle(Request $request, Closure $next, ...$args): Response 32 | { 33 | $lifetimeInSeconds = $this->getLifetime($args); 34 | $tags = $this->getTags($args); 35 | 36 | if ($this->responseCache->enabled($request) && ! $this->responseCache->shouldBypass($request)) { 37 | try { 38 | if ($this->responseCache->hasBeenCached($request, $tags)) { 39 | 40 | $response = $this->getCachedResponse($request, $tags); 41 | if ($response !== false) { 42 | return $response; 43 | } 44 | } 45 | } catch (CouldNotUnserialize $e) { 46 | report("Could not unserialize response, returning uncached response instead. Error: {$e->getMessage()}"); 47 | event(new CacheMissed($request)); 48 | } 49 | } 50 | 51 | $response = $next($request); 52 | 53 | if ($this->responseCache->enabled($request) && ! $this->responseCache->shouldBypass($request)) { 54 | if ($this->responseCache->shouldCache($request, $response)) { 55 | $this->makeReplacementsAndCacheResponse($request, $response, $lifetimeInSeconds, $tags); 56 | } 57 | } 58 | 59 | event(new CacheMissed($request)); 60 | 61 | return $response; 62 | } 63 | 64 | protected function getCachedResponse(Request $request, array $tags = []): false|Response 65 | { 66 | try { 67 | $response = $this->responseCache->getCachedResponseFor($request, $tags); 68 | } catch (CouldNotUnserialize $exception) { 69 | throw $exception; 70 | } catch (Throwable $exception) { 71 | report("Unable to retrieve cached response when one was expected. Error: {$exception->getMessage()}"); 72 | 73 | return false; 74 | } 75 | 76 | event(new ResponseCacheHit($request)); 77 | 78 | $response = $this->addCacheAgeHeader($response); 79 | 80 | $this->getReplacers()->each(function (Replacer $replacer) use ($response) { 81 | $replacer->replaceInCachedResponse($response); 82 | }); 83 | 84 | return $response; 85 | } 86 | 87 | protected function makeReplacementsAndCacheResponse( 88 | Request $request, 89 | Response $response, 90 | ?int $lifetimeInSeconds = null, 91 | array $tags = [] 92 | ): void { 93 | $cachedResponse = clone $response; 94 | 95 | $this->getReplacers()->each(fn (Replacer $replacer) => $replacer->prepareResponseToCache($cachedResponse)); 96 | 97 | $this->responseCache->cacheResponse($request, $cachedResponse, $lifetimeInSeconds, $tags); 98 | } 99 | 100 | protected function getReplacers(): Collection 101 | { 102 | return collect(config('responsecache.replacers')) 103 | ->map(fn (string $replacerClass) => app($replacerClass)); 104 | } 105 | 106 | protected function getLifetime(array $args): ?int 107 | { 108 | if (count($args) >= 1 && is_numeric($args[0])) { 109 | return (int) $args[0]; 110 | } 111 | 112 | return null; 113 | } 114 | 115 | protected function getTags(array $args): array 116 | { 117 | $tags = $args; 118 | 119 | if (count($args) >= 1 && is_numeric($args[0])) { 120 | $tags = array_slice($args, 1); 121 | } 122 | 123 | return array_filter($tags); 124 | } 125 | 126 | public function addCacheAgeHeader(Response $response): Response 127 | { 128 | if (config('responsecache.add_cache_age_header') and $time = $response->headers->get(config('responsecache.cache_time_header_name'))) { 129 | $ageInSeconds = (int) Carbon::parse($time)->diffInSeconds(Carbon::now(), true); 130 | 131 | $response->headers->set(config('responsecache.cache_age_header_name'), $ageInSeconds); 132 | } 133 | 134 | return $response; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-responsecache` will be documented in this file. 4 | 5 | ## 7.7.2 - 2025-08-28 6 | 7 | ### What's Changed 8 | 9 | * Fix Carbon3 diffInSeconds to use int and not float by @it-can in https://github.com/spatie/laravel-responsecache/pull/500 10 | * Bump actions/checkout from 4 to 5 by @dependabot[bot] in https://github.com/spatie/laravel-responsecache/pull/498 11 | * Bump stefanzweifel/git-auto-commit-action from 5.2.0 to 6.0.1 by @dependabot[bot] in https://github.com/spatie/laravel-responsecache/pull/496 12 | 13 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.7.1...7.7.2 14 | 15 | ## 7.7.1 - 2025-07-17 16 | 17 | ### What's Changed 18 | 19 | * Introduce using() method to conveniently define lifetime and tags by @kamilkozak in https://github.com/spatie/laravel-responsecache/pull/491 20 | 21 | ### New Contributors 22 | 23 | * @kamilkozak made their first contribution in https://github.com/spatie/laravel-responsecache/pull/491 24 | 25 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.7.0...7.7.1 26 | 27 | ## 7.7.0 - 2025-05-20 28 | 29 | ### What's Changed 30 | 31 | * Bump stefanzweifel/git-auto-commit-action from 5.1.0 to 5.2.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/493 32 | * Bump dependabot/fetch-metadata from 2.3.0 to 2.4.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/494 33 | * [8.0] Introduce `ClearingResponseCacheFailed` and return boolean by `clear()` by @alies-dev in https://github.com/spatie/laravel-responsecache/pull/495 34 | 35 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.5...7.7.0 36 | 37 | ## 7.6.5 - 2025-04-10 38 | 39 | ### What's Changed 40 | 41 | * fix DocBlock in ResponseCache Facade by @ricventu in https://github.com/spatie/laravel-responsecache/pull/492 42 | 43 | ### New Contributors 44 | 45 | * @ricventu made their first contribution in https://github.com/spatie/laravel-responsecache/pull/492 46 | 47 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.4...7.6.5 48 | 49 | ## 7.6.4 - 2025-02-25 50 | 51 | ### What's Changed 52 | 53 | * Bump stefanzweifel/git-auto-commit-action from 5.0.1 to 5.1.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/487 54 | * Bump dependabot/fetch-metadata from 2.2.0 to 2.3.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/488 55 | * Add compatibility for Laravel 12. by @mazedlx in https://github.com/spatie/laravel-responsecache/pull/490 56 | 57 | ### New Contributors 58 | 59 | * @mazedlx made their first contribution in https://github.com/spatie/laravel-responsecache/pull/490 60 | 61 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.3...7.6.4 62 | 63 | ## 7.6.3 - 2024-12-11 64 | 65 | ### What's Changed 66 | 67 | * Fix ResponseCache Facade docblock for forget() by @alexcanana in https://github.com/spatie/laravel-responsecache/pull/486 68 | 69 | ### New Contributors 70 | 71 | * @alexcanana made their first contribution in https://github.com/spatie/laravel-responsecache/pull/486 72 | 73 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.2...7.6.3 74 | 75 | ## 7.6.2 - 2024-12-09 76 | 77 | ### What's Changed 78 | 79 | * CsrfTokenReplacer avoid str_replace(): by @thyseus in https://github.com/spatie/laravel-responsecache/pull/482 80 | 81 | ### New Contributors 82 | 83 | * @thyseus made their first contribution in https://github.com/spatie/laravel-responsecache/pull/482 84 | 85 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.1...7.6.2 86 | 87 | ## 7.6.1 - 2024-11-18 88 | 89 | ### What's Changed 90 | 91 | * Add PHPDoc for ResponseCache Facade by @alies-dev in https://github.com/spatie/laravel-responsecache/pull/485 92 | 93 | ### New Contributors 94 | 95 | * @alies-dev made their first contribution in https://github.com/spatie/laravel-responsecache/pull/485 96 | 97 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.6.0...7.6.1 98 | 99 | ## 7.6.0 - 2024-08-05 100 | 101 | ### What's Changed 102 | 103 | * Bump stefanzweifel/git-auto-commit-action from 5.0.0 to 5.0.1 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/470 104 | * Fix incorrect grammar by @SebKay in https://github.com/spatie/laravel-responsecache/pull/472 105 | * Add missing Laravel 11.x Documentation by @omaratpxt in https://github.com/spatie/laravel-responsecache/pull/474 106 | * Bump dependabot/fetch-metadata from 1.6.0 to 2.2.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/475 107 | * Switch DefaultHasher to xxh128 for a faster alternative to MD5. by @marcell-ferenc in https://github.com/spatie/laravel-responsecache/pull/478 108 | 109 | ### New Contributors 110 | 111 | * @SebKay made their first contribution in https://github.com/spatie/laravel-responsecache/pull/472 112 | * @omaratpxt made their first contribution in https://github.com/spatie/laravel-responsecache/pull/474 113 | * @marcell-ferenc made their first contribution in https://github.com/spatie/laravel-responsecache/pull/478 114 | 115 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.5.2...7.6.0 116 | 117 | ## 7.5.2 - 2024-04-03 118 | 119 | ### What's Changed 120 | 121 | * Cast cache lifetime to integer by default by @dwightwatson in https://github.com/spatie/laravel-responsecache/pull/468 122 | 123 | ### New Contributors 124 | 125 | * @dwightwatson made their first contribution in https://github.com/spatie/laravel-responsecache/pull/468 126 | 127 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.5.1...7.5.2 128 | 129 | ## 7.5.1 - 2024-03-23 130 | 131 | - allow Carbon 3 132 | 133 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.5.0...7.5.1 134 | 135 | ## 7.5.0 - 2024-03-11 136 | 137 | ### What's Changed 138 | 139 | * Add php 8.3 matrix by @binbyz in https://github.com/spatie/laravel-responsecache/pull/460 140 | * Support Laravel 11 141 | 142 | ### New Contributors 143 | 144 | * @binbyz made their first contribution in https://github.com/spatie/laravel-responsecache/pull/460 145 | 146 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.10...7.5.0 147 | 148 | ## 7.4.10 - 2023-10-28 149 | 150 | ### What's Changed 151 | 152 | - Bump actions/checkout from 3 to 4 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/449 153 | - Bump stefanzweifel/git-auto-commit-action from 4.16.0 to 5.0.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/452 154 | - Do not enable cache time header by default by @francoism90 in https://github.com/spatie/laravel-responsecache/pull/456 155 | 156 | ### New Contributors 157 | 158 | - @francoism90 made their first contribution in https://github.com/spatie/laravel-responsecache/pull/456 159 | 160 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.9...7.4.10 161 | 162 | ## 7.4.9 - 2023-10-02 163 | 164 | - Fix docblock 165 | 166 | ## 7.4.8 - 2023-09-27 167 | 168 | ### What's Changed 169 | 170 | - Change PHPDoc for method clear(array ) in Facade by @kra-so in https://github.com/spatie/laravel-responsecache/pull/451 171 | 172 | ### New Contributors 173 | 174 | - @gomzyakov made their first contribution in https://github.com/spatie/laravel-responsecache/pull/437 175 | - @kra-so made their first contribution in https://github.com/spatie/laravel-responsecache/pull/451 176 | 177 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.7...7.4.8 178 | 179 | ## 7.4.7 - 2023-04-07 180 | 181 | ### What's Changed 182 | 183 | - Bump dependabot/fetch-metadata from 1.3.5 to 1.3.6 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/427 184 | - Property name changed by @Fot0n in https://github.com/spatie/laravel-responsecache/pull/428 185 | - Typo fix in config responsecache by @dorqa95 in https://github.com/spatie/laravel-responsecache/pull/431 186 | - Issue #342: CacheResponse race condition with has and get by @swichers in https://github.com/spatie/laravel-responsecache/pull/434 187 | 188 | ### New Contributors 189 | 190 | - @Fot0n made their first contribution in https://github.com/spatie/laravel-responsecache/pull/428 191 | - @dorqa95 made their first contribution in https://github.com/spatie/laravel-responsecache/pull/431 192 | - @swichers made their first contribution in https://github.com/spatie/laravel-responsecache/pull/434 193 | 194 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.6...7.4.7 195 | 196 | ## 7.4.6 - 2023-01-23 197 | 198 | - add support for L10 199 | 200 | ## 7.4.5 - 2023-01-23 201 | 202 | ### What's Changed 203 | 204 | - Bump stefanzweifel/git-auto-commit-action from 4.15.4 to 4.16.0 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/424 205 | - Normalize query string parameters before hashing by @cosmastech in https://github.com/spatie/laravel-responsecache/pull/426 206 | 207 | ### New Contributors 208 | 209 | - @cosmastech made their first contribution in https://github.com/spatie/laravel-responsecache/pull/426 210 | 211 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.4...7.4.5 212 | 213 | ## 7.4.4 - 2022-11-25 214 | 215 | ### What's Changed 216 | 217 | - Refactor tests to pest by @AyoobMH in https://github.com/spatie/laravel-responsecache/pull/418 218 | - Add PHP 8.2 Support to tests workflow by @patinthehat in https://github.com/spatie/laravel-responsecache/pull/421 219 | - Add Dependabot Automation by @patinthehat in https://github.com/spatie/laravel-responsecache/pull/420 220 | - Bump actions/checkout from 2 to 3 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/423 221 | - Bump stefanzweifel/git-auto-commit-action from 2.3.0 to 4.15.4 by @dependabot in https://github.com/spatie/laravel-responsecache/pull/422 222 | - Catch CouldNotUnserialize exception and continue returning a response by @roberttolton in https://github.com/spatie/laravel-responsecache/pull/408 223 | 224 | ### New Contributors 225 | 226 | - @AyoobMH made their first contribution in https://github.com/spatie/laravel-responsecache/pull/418 227 | - @dependabot made their first contribution in https://github.com/spatie/laravel-responsecache/pull/423 228 | - @roberttolton made their first contribution in https://github.com/spatie/laravel-responsecache/pull/408 229 | 230 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.3...7.4.4 231 | 232 | ## 7.4.3 - 2022-09-24 233 | 234 | ### What's Changed 235 | 236 | - Dispatch clear events when using facade by @mateusjunges in https://github.com/spatie/laravel-responsecache/pull/413 237 | 238 | ### New Contributors 239 | 240 | - @mateusjunges made their first contribution in https://github.com/spatie/laravel-responsecache/pull/413 241 | 242 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.2...7.4.3 243 | 244 | ## 7.4.2 - 2022-09-02 245 | 246 | ### What's Changed 247 | 248 | - Always prepend app url to requests by @apeisa in https://github.com/spatie/laravel-responsecache/pull/409 249 | 250 | ### New Contributors 251 | 252 | - @apeisa made their first contribution in https://github.com/spatie/laravel-responsecache/pull/409 253 | 254 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.1...7.4.2 255 | 256 | ## 7.4.1 - 2022-08-09 257 | 258 | ### What's Changed 259 | 260 | - Cache bypass header now also prevents an already cached response from being returned by @fgilio in https://github.com/spatie/laravel-responsecache/pull/407 261 | 262 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.4.0...7.4.1 263 | 264 | ## 7.4.0 - 2022-08-01 265 | 266 | ### What's Changed 267 | 268 | - Add cache bypass header by @fgilio in https://github.com/spatie/laravel-responsecache/pull/406 269 | 270 | ### New Contributors 271 | 272 | - @fgilio made their first contribution in https://github.com/spatie/laravel-responsecache/pull/406 273 | 274 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.3.1...7.4.0 275 | 276 | ## 7.3.1 - 2022-05-30 277 | 278 | ### What's Changed 279 | 280 | - Handle missed cache gracefully by @antennaio in https://github.com/spatie/laravel-responsecache/pull/383 281 | 282 | ### New Contributors 283 | 284 | - @antennaio made their first contribution in https://github.com/spatie/laravel-responsecache/pull/383 285 | 286 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.3.0...7.3.1 287 | 288 | ## 7.3.0 - 2022-05-16 289 | 290 | ## What's Changed 291 | 292 | - Add option to output cache age header by @it-can in https://github.com/spatie/laravel-responsecache/pull/385 293 | 294 | ## New Contributors 295 | 296 | - @it-can made their first contribution in https://github.com/spatie/laravel-responsecache/pull/385 297 | 298 | **Full Changelog**: https://github.com/spatie/laravel-responsecache/compare/7.2.0...7.3.0 299 | 300 | ## 7.2.0 - 2022-01-13 301 | 302 | - support Laravel 9 303 | 304 | ## 7.1.0 - 2021-04-27 305 | 306 | - add `CacheItemSelector` 307 | 308 | ## 7.0.1 - 2021-04-13 309 | 310 | - add `url` option to `ClearCommand` (#348) 311 | 312 | ## 7.0.0 - 2021-04-02 313 | 314 | - require PHP 8+ 315 | - drop support for PHP 7.x 316 | - use PHP 8 syntax where possible 317 | 318 | ## 6.6.9 - 2021-03-30 319 | 320 | - fix for issue #331 (#344) 321 | 322 | ## 6.6.8 - 2020-01-25 323 | 324 | - use package service provider 325 | 326 | ## 6.6.7 - 2020-11-28 327 | 328 | - add support for PHP 8 329 | 330 | ## 6.6.6 - 2020-09-27 331 | 332 | - fix clearing tagged cache 333 | 334 | ## 6.6.5 - 2020-09-22 335 | 336 | - fix tagged responsecache:clear (#316) 337 | 338 | ## 6.6.4 - 2020-09-09 339 | 340 | - Support Laravel 8 341 | 342 | ## 6.6.3 - 2020-08-24 343 | 344 | - replace Laravel/framework with individual packages (#304) 345 | 346 | ## 6.6.2 - 2020-06-03 347 | 348 | - support JSON types other than application/json (#299) 349 | 350 | ## 6.6.1 - 2020-04-22 351 | 352 | - change to the proper way of setting app URL on runtime (#290) 353 | 354 | ## 6.6.0 - 2020-03-02 355 | 356 | - drop support for Laravel 6 to fix the test suite (namespace of `TestResponse` has changed) 357 | 358 | ## 6.5.0 - 2020-03-02 359 | 360 | - add support for Laravel 7 361 | 362 | ## 6.4.0 - 2019-12-01 363 | 364 | - drop support for all non-current PHP and Laravel versions 365 | 366 | ## 6.3.0 - 2019-09-01 367 | 368 | - add support for custom serializers 369 | 370 | ## 6.2.1 - 2020-03-07 371 | 372 | - make compatible with Laravel 7, so the package can be used on PHP 7.3 373 | 374 | ## 6.2.0 - 2019-09-01 375 | 376 | - make compatible with Laravel 6 377 | 378 | ## 6.1.1 - 2019-08-08 379 | 380 | - restore laravel 5.7 compatibility 381 | 382 | ## 6.1.0 - 2019-08-01 383 | 384 | - add support for cache tags 385 | 386 | ## 6.0.2 - 2019-07-31 387 | 388 | - make json responses cacheable 389 | 390 | ## 6.0.1 - 2019-07-09 391 | 392 | - use Rfc2822S formatted date in cache time header 393 | 394 | ## 6.0.0 - 2019-05-20 395 | 396 | - added support for replacers 397 | - you can now swap out `RequestHasher` in favor of a custom one 398 | - `CacheAllSuccessfulGetRequests` will only cache responses of which the content type starts with `text` 399 | - removed deprecated `Flush` command 400 | - `\Spatie\ResponseCache\ResponseCacheFacade` has been removed 401 | - dropped support for carbon v1 402 | - dropped support for PHP 7.2 403 | 404 | ## 5.0.3 - 2019-05-10 405 | 406 | - make sure the request starts with the app url - fixes #177 407 | 408 | ## 5.0.2 - 2019-04-05 409 | 410 | - make host specific caches 411 | 412 | ## 5.0.1 - 2019-03-15 413 | 414 | - fix cache lifetime in config file 415 | 416 | ## 5.0.0 - 2019-02-27 417 | 418 | - drop support for Laravel 5.7 and lower 419 | - drop support for PHP 7.0 and lower 420 | - change all cache time parameters to seconds (see UPGRADING.md) 421 | 422 | ## 4.4.5 - 2019-02-27 423 | 424 | - add support for Laravel 5.8 425 | - you can no longer add multiple `CacheResponse` middleware to one route 426 | 427 | ## 4.4.4 - 2018-09-23 428 | 429 | - fix for caching urls with query parameters 430 | 431 | ## 4.4.3 - 2018-09-23 432 | 433 | - fix for forgetting a specific url 434 | 435 | ## 4.4.2 - 2018-08-24 436 | 437 | - add support for Laravel 5.7 438 | 439 | ## 4.4.1 - 2018-07-26 440 | 441 | - fix for issue #123 442 | 443 | ## 4.4.0 - 2018-04-30 444 | 445 | - add support for Lumen 446 | 447 | ## 4.3.0 - 2018-03-01 448 | 449 | - add `forget` 450 | 451 | ## 4.2.1 - 2018-02-08 452 | 453 | - add support for L5.6 454 | 455 | ## 4.2.0 - 2018-01-30 456 | 457 | - Added: `clear()` method and `responsecache:clear` command 458 | - Deprecated: `flush()` method and `responsecache:flush` command 459 | 460 | Deprecated features will still work until the next major version. 461 | 462 | ## 4.1.1 - 2018-01-30 463 | 464 | - Added: Better exception handling when something goes wrong unserializing the response 465 | 466 | ## 4.1.0 - 2017-09-26 467 | 468 | - Added: Support for specific lifetimes on routes 469 | 470 | ## 4.0.1 - 2017-08-30 471 | 472 | - Fixed: Artisan command registration 473 | 474 | ## 4.0.0 - 2017-08-30 475 | 476 | - Added: Support for Laravel 5.5 477 | - Removed: Support for all older Laravel versions 478 | - Changed: Renamed facade class 479 | 480 | ## 3.2.0 - 2017-06-19 481 | 482 | - Added: Support for `BinaryFileResponse` 483 | 484 | ## 3.1.0 - 2017-04-28 485 | 486 | - Added: Support for taggable cache 487 | 488 | ## 3.0.1 - 2017-03-16 489 | 490 | - Fixed: Php version dependency in `composer.json` 491 | 492 | ## 3.0.0 - 2017-03-16 493 | 494 | - Added: `enabled` method on cache profiles 495 | - Added: Events 496 | - Changed: Middleware won't automatically be registered anymore 497 | - Changed: Renamed config file 498 | - Changed: Renamed various methods for readability 499 | - Removed: Dropped PHP 5.6 support 500 | 501 | ## 2.0.0 - 2017-01-24 502 | 503 | - Added: Support for Laravel 5.4 504 | - Removed: Dropped support for all older Laravel versions 505 | 506 | ## 1.1.7 - 2016-10-10 507 | 508 | - Added: Usage of `RESPONSE_CACHE_LIFETIME` env var to config file 509 | 510 | ## 1.1.6 - 2016-08-07 511 | 512 | - Changed: Debug headers will not be sent when `APP_DEBUG` is set to false 513 | 514 | ## 1.1.5 - 2015-08-28 515 | 516 | - Fixed: Issue where the cache middleware couldn't correctly determine the currently authenticated user 517 | 518 | ## 1.1.4 - 2015-08-12 519 | 520 | - Fixed: An issue where cached request were still served even if the package was disabled via the config file 521 | 522 | ## 1.1.3 - 2015-08-11 523 | 524 | - Fixed: An issue where the cache header could not be set 525 | 526 | ## 1.1.2 - 2015-07-22 527 | 528 | - Fixed: BaseCacheProfile has been made abstract (as it should have been all along) 529 | 530 | ## 1.1.1 - 2015-07-20 531 | 532 | - Fixed: Default cachetime 533 | 534 | ## 1.1.0 - 2015-07-20 535 | 536 | - Added: A command to flush the response cache 537 | 538 | ## 1.0.0 - 2015-07-20 539 | 540 | - Initial release 541 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | Logo for laravel-responsecache 6 | 7 | 8 | 9 |

Speed up an app by caching the entire response

10 | 11 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-responsecache.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-responsecache) 12 | [![MIT Licensed](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 13 | ![Tests](https://github.com/spatie/laravel-responsecache/actions/workflows/run-tests.yml/badge.svg) 14 | [![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-responsecache.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-responsecache) 15 | 16 |
17 | 18 | This Laravel package can cache an entire response. By default it will cache all successful get-requests that return text based content (such as html and json) for a week. This could potentially speed up the response quite considerably. 19 | 20 | So the first time a request comes in the package will save the response before sending it to the users. When the same request comes in again we're not going through the entire application but just respond with the saved response. 21 | 22 | Are you a visual learner? Then watch [this video](https://spatie.be/videos/laravel-package-training/laravel-responsecache) that covers how you can use laravel-responsecache and how it works under the hood. 23 | 24 | ## Support us 25 | 26 | [](https://spatie.be/github-ad-click/laravel-responsecache) 27 | 28 | We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us). 29 | 30 | We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards). 31 | 32 | ## Installation 33 | 34 | > If you're using PHP 7, install v6.x of this package. 35 | 36 | You can install the package via composer: 37 | ```bash 38 | composer require spatie/laravel-responsecache 39 | ``` 40 | 41 | The package will automatically register itself. 42 | 43 | You can publish the config file with: 44 | ```bash 45 | php artisan vendor:publish --tag="responsecache-config" 46 | ``` 47 | 48 | This is the contents of the published config file: 49 | 50 | ```php 51 | // config/responsecache.php 52 | 53 | return [ 54 | /* 55 | * Determine if the response cache middleware should be enabled. 56 | */ 57 | 'enabled' => env('RESPONSE_CACHE_ENABLED', true), 58 | 59 | /* 60 | * The given class will determinate if a request should be cached. The 61 | * default class will cache all successful GET-requests. 62 | * 63 | * You can provide your own class given that it implements the 64 | * CacheProfile interface. 65 | */ 66 | 'cache_profile' => Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests::class, 67 | 68 | /* 69 | * Optionally, you can specify a header that will force a cache bypass. 70 | * This can be useful to monitor the performance of your application. 71 | */ 72 | 'cache_bypass_header' => [ 73 | 'name' => env('CACHE_BYPASS_HEADER_NAME', null), 74 | 'value' => env('CACHE_BYPASS_HEADER_VALUE', null), 75 | ], 76 | 77 | /* 78 | * When using the default CacheRequestFilter this setting controls the 79 | * default number of seconds responses must be cached. 80 | */ 81 | 'cache_lifetime_in_seconds' => env('RESPONSE_CACHE_LIFETIME', 60 * 60 * 24 * 7), 82 | 83 | /* 84 | * This setting determines if a http header named with the cache time 85 | * should be added to a cached response. This can be handy when 86 | * debugging. 87 | */ 88 | 'add_cache_time_header' => env('APP_DEBUG', true), 89 | 90 | /* 91 | * This setting determines the name of the http header that contains 92 | * the time at which the response was cached 93 | */ 94 | 'cache_time_header_name' => env('RESPONSE_CACHE_HEADER_NAME', 'laravel-responsecache'), 95 | 96 | /* 97 | * This setting determines if a http header named with the cache age 98 | * should be added to a cached response. This can be handy when 99 | * debugging. 100 | * ONLY works when "add_cache_time_header" is also active! 101 | */ 102 | 'add_cache_age_header' => env('RESPONSE_CACHE_AGE_HEADER', false), 103 | 104 | /* 105 | * This setting determines the name of the http header that contains 106 | * the age of cache 107 | */ 108 | 'cache_age_header_name' => env('RESPONSE_CACHE_AGE_HEADER_NAME', 'laravel-responsecache-age'), 109 | 110 | /* 111 | * Here you may define the cache store that should be used to store 112 | * requests. This can be the name of any store that is 113 | * configured in app/config/cache.php 114 | */ 115 | 'cache_store' => env('RESPONSE_CACHE_DRIVER', 'file'), 116 | 117 | /* 118 | * Here you may define replacers that dynamically replace content from the response. 119 | * Each replacer must implement the Replacer interface. 120 | */ 121 | 'replacers' => [ 122 | \Spatie\ResponseCache\Replacers\CsrfTokenReplacer::class, 123 | ], 124 | 125 | /* 126 | * If the cache driver you configured supports tags, you may specify a tag name 127 | * here. All responses will be tagged. When clearing the responsecache only 128 | * items with that tag will be flushed. 129 | * 130 | * You may use a string or an array here. 131 | */ 132 | 'cache_tag' => '', 133 | 134 | /* 135 | * This class is responsible for generating a hash for a request. This hash 136 | * is used to look up an cached response. 137 | */ 138 | 'hasher' => \Spatie\ResponseCache\Hasher\DefaultHasher::class, 139 | 140 | /* 141 | * This class is responsible for serializing responses. 142 | */ 143 | 'serializer' => \Spatie\ResponseCache\Serializers\DefaultSerializer::class, 144 | ]; 145 | ``` 146 | 147 | And finally you should install the provided middlewares `\Spatie\ResponseCache\Middlewares\CacheResponse::class` and `\Spatie\ResponseCache\Middlewares\DoNotCacheResponse`. 148 | 149 | 150 | **For laravel 11.x and newer:** 151 | 152 | Add the middleware definitions to the bootstrap app. 153 | 154 | ```php 155 | // bootstrap/app.php 156 | 157 | 158 | ->withMiddleware(function (Middleware $middleware) { 159 | ... 160 | $middleware->web(append: [ 161 | ... 162 | \Spatie\ResponseCache\Middlewares\CacheResponse::class, 163 | ]); 164 | 165 | ... 166 | 167 | $middleware->alias([ 168 | ... 169 | 'doNotCacheResponse' => \Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class, 170 | ]); 171 | }) 172 | 173 | ``` 174 | 175 | **For laravel 10.x and earlier:** 176 | 177 | Add the middleware definitions to the http kernel. 178 | 179 | 180 | ```php 181 | // app/Http/Kernel.php 182 | 183 | ... 184 | 185 | protected $middlewareGroups = [ 186 | 'web' => [ 187 | ... 188 | \Spatie\ResponseCache\Middlewares\CacheResponse::class, 189 | ], 190 | 191 | ... 192 | 193 | protected $middlewareAliases = [ 194 | ... 195 | 'doNotCacheResponse' => \Spatie\ResponseCache\Middlewares\DoNotCacheResponse::class, 196 | ]; 197 | 198 | ``` 199 | 200 | ## Usage 201 | 202 | ### Basic usage 203 | 204 | By default, the package will cache all successful `GET` requests for a week. 205 | Logged in users will each have their own separate cache. If this behaviour is what you 206 | need, you're done: installing the `ResponseCacheServiceProvider` was enough. 207 | 208 | ### Clearing the cache 209 | 210 | #### Manually 211 | 212 | The entire cache can be cleared with: 213 | ```php 214 | ResponseCache::clear(); 215 | ``` 216 | This will clear everything from the cache store specified in the config file. 217 | 218 | #### Using a console command 219 | 220 | The same can be accomplished by issuing this artisan command: 221 | 222 | ```bash 223 | php artisan responsecache:clear 224 | ``` 225 | 226 | #### Using model events 227 | 228 | You can leverage model events to clear the cache whenever a model is saved or deleted. Here's an example. 229 | ```php 230 | namespace App\Traits; 231 | 232 | use Spatie\ResponseCache\Facades\ResponseCache; 233 | 234 | trait ClearsResponseCache 235 | { 236 | public static function bootClearsResponseCache() 237 | { 238 | self::created(function () { 239 | ResponseCache::clear(); 240 | }); 241 | 242 | self::updated(function () { 243 | ResponseCache::clear(); 244 | }); 245 | 246 | self::deleted(function () { 247 | ResponseCache::clear(); 248 | }); 249 | } 250 | } 251 | ``` 252 | 253 | ### Forget one or several specific URIs 254 | 255 | You can forget specific URIs with: 256 | ```php 257 | // Forget one 258 | ResponseCache::forget('/some-uri'); 259 | 260 | // Forget several 261 | ResponseCache::forget(['/some-uri', '/other-uri']); 262 | 263 | // Equivalent to the example above 264 | ResponseCache::forget('/some-uri', '/other-uri'); 265 | ``` 266 | 267 | The `ResponseCache::forget` method only works when you're not using a `cacheNameSuffix` in your cache profile, 268 | use `ResponseCache::selectCachedItems` to deal with `cacheNameSuffix`. 269 | 270 | ### Forgetting a selection of cached items 271 | 272 | You can use `ResponseCache::selectCachedItems()` to specify which cached items should be forgotten. 273 | 274 | ```php 275 | 276 | // forgetting all PUT responses of /some-uri 277 | ResponseCache::selectCachedItems()->withPutMethod()->forUrls('/some-uri')->forget(); 278 | 279 | // forgetting all PUT responses of multiple endpoints 280 | ResponseCache::selectCachedItems()->withPutMethod()->forUrls(['/some-uri','/other-uri'])->forget(); 281 | 282 | // this is equivalent to the example above 283 | ResponseCache::selectCachedItems()->withPutMethod()->forUrls('/some-uri','/other-uri')->forget(); 284 | 285 | // forget /some-uri cached with "100" suffix (by default suffix is user->id or "") 286 | ResponseCache::selectCachedItems()->usingSuffix('100')->forUrls('/some-uri')->forget(); 287 | 288 | // all options combined 289 | ResponseCache::selectCachedItems() 290 | ->withPutMethod() 291 | ->withHeaders(['foo'=>'bar']) 292 | ->withCookies(['cookie1' => 'value']) 293 | ->withParameters(['param1' => 'value']) 294 | ->withRemoteAddress('127.0.0.1') 295 | ->usingSuffix('100') 296 | ->usingTags('tag1', 'tag2') 297 | ->forUrls('/some-uri', '/other-uri') 298 | ->forget(); 299 | 300 | ``` 301 | 302 | The `cacheNameSuffix` depends by your cache profile, by default is the user ID or an empty string if not authenticated. 303 | 304 | ### Preventing a request from being cached 305 | 306 | Requests can be ignored by using the `doNotCacheResponse` middleware. 307 | This middleware [can be assigned to routes and controllers](http://laravel.com/docs/master/controllers#controller-middleware). 308 | 309 | Using the middleware our route could be exempt from being cached. 310 | 311 | ```php 312 | // app/Http/routes.php 313 | 314 | Route::get('/auth/logout', ['middleware' => 'doNotCacheResponse', 'uses' => 'AuthController@getLogout']); 315 | ``` 316 | 317 | Alternatively, you can add the middleware to a controller: 318 | 319 | ```php 320 | class UserController extends Controller 321 | { 322 | public function __construct() 323 | { 324 | $this->middleware('doNotCacheResponse', ['only' => ['fooAction', 'barAction']]); 325 | } 326 | } 327 | ``` 328 | 329 | ### Purposefully bypassing the cache 330 | 331 | It's possible to purposefully and securely bypass the cache and ensure you always receive a fresh response. This may be useful in case you want to profile some endpoint or in case you need to debug a response. 332 | In any case, all you need to do is fill the `CACHE_BYPASS_HEADER_NAME` and `CACHE_BYPASS_HEADER_VALUE` environment variables and then use that custom header when performing the requests. 333 | 334 | ### Creating a custom cache profile 335 | To determine which requests should be cached, and for how long, a cache profile class is used. 336 | The default class that handles these questions is `Spatie\ResponseCache\CacheProfiles\CacheAllSuccessfulGetRequests`. 337 | 338 | You can create your own cache profile class by implementing the ` 339 | Spatie\ResponseCache\CacheProfiles\CacheProfile` interface. Let's take a look at the interface: 340 | 341 | ```php 342 | interface CacheProfile 343 | { 344 | /* 345 | * Determine if the response cache middleware should be enabled. 346 | */ 347 | public function enabled(Request $request): bool; 348 | 349 | /* 350 | * Determine if the given request should be cached. 351 | */ 352 | public function shouldCacheRequest(Request $request): bool; 353 | 354 | /* 355 | * Determine if the given response should be cached. 356 | */ 357 | public function shouldCacheResponse(Response $response): bool; 358 | 359 | /* 360 | * Return the time when the cache must be invalidated. 361 | */ 362 | public function cacheRequestUntil(Request $request): DateTime; 363 | 364 | /** 365 | * Return a string to differentiate this request from others. 366 | * 367 | * For example: if you want a different cache per user you could return the id of 368 | * the logged in user. 369 | * 370 | * @param \Illuminate\Http\Request $request 371 | * 372 | * @return mixed 373 | */ 374 | public function useCacheNameSuffix(Request $request); 375 | } 376 | ``` 377 | 378 | ### Caching specific routes 379 | Instead of registering the `cacheResponse` middleware globally, you can also register it as route middleware. 380 | 381 | ```php 382 | protected $middlewareAliases = [ 383 | ... 384 | 'cacheResponse' => \Spatie\ResponseCache\Middlewares\CacheResponse::class, 385 | ]; 386 | ``` 387 | 388 | When using the route middleware you can specify the number of seconds these routes should be cached: 389 | 390 | ```php 391 | // cache this route for 5 minutes 392 | Route::get('/my-special-snowflake', 'SnowflakeController@index')->middleware('cacheResponse:300'); 393 | 394 | // cache all these routes for 10 minutes 395 | Route::group(function() { 396 | Route::get('/another-special-snowflake', 'AnotherSnowflakeController@index'); 397 | 398 | Route::get('/yet-another-special-snowflake', 'YetAnotherSnowflakeController@index'); 399 | })->middleware('cacheResponse:600'); 400 | ``` 401 | 402 | ### Using tags 403 | 404 | If the [cache driver you configured supports tags](https://laravel.com/docs/5.8/cache#cache-tags), you can specify a list of tags when applying the middleware. 405 | 406 | ```php 407 | use Spatie\ResponseCache\Middlewares\CacheResponse; 408 | 409 | // add a "foo" tag to this route with a 300 second lifetime 410 | Route::get('/test1', 'SnowflakeController@index')->middleware('cacheResponse:300,foo'); 411 | 412 | // add a "bar" tag to this route 413 | Route::get('/test2', 'SnowflakeController@index')->middleware('cacheResponse:bar'); 414 | 415 | // add both "foo" and "bar" tags to these routes 416 | Route::group(function() { 417 | Route::get('/test3', 'AnotherSnowflakeController@index'); 418 | 419 | Route::get('/test4', 'YetAnotherSnowflakeController@index'); 420 | })->middleware('cacheResponse:foo,bar'); 421 | 422 | // or use the using method for convenience 423 | Route::get('/test5', 'SnowflakeController@index')->middleware(CacheResponse::using(300, 'foo', 'bar')); 424 | ``` 425 | 426 | #### Clearing tagged content 427 | 428 | You can clear responses which are assigned a tag or list of tags. For example, this statement would remove the `'/test3'` and `'/test4'` routes above: 429 | 430 | ```php 431 | ResponseCache::clear(['foo', 'bar']); 432 | ``` 433 | 434 | In contrast, this statement would remove only the `'/test2'` route: 435 | 436 | ```php 437 | ResponseCache::clear(['bar']); 438 | ``` 439 | 440 | Note that this uses [Laravel's built in cache tags](https://laravel.com/docs/master/cache#cache-tags) functionality, meaning 441 | routes can also be cleared in the usual way: 442 | 443 | ```php 444 | Cache::tags('special')->flush(); 445 | ``` 446 | 447 | ### Events 448 | 449 | There are several events you can use to monitor and debug response caching in your application. 450 | 451 | #### ResponseCacheHit 452 | 453 | `Spatie\ResponseCache\Events\ResponseCacheHit` 454 | 455 | This event is fired when a request passes through the `ResponseCache` middleware and a cached response was found and returned. 456 | 457 | #### CacheMissed 458 | 459 | `Spatie\ResponseCache\Events\CacheMissed` 460 | 461 | This event is fired when a request passes through the `ResponseCache` middleware but no cached response was found or returned. 462 | 463 | #### ClearingResponseCache, ClearedResponseCache and ClearingResponseCacheFailed 464 | 465 | 1. `Spatie\ResponseCache\Events\ClearingResponseCache` 466 | 2. `Spatie\ResponseCache\Events\ClearedResponseCache` or `Spatie\ResponseCache\Events\ClearingResponseCacheFailed` 467 | 468 | These events are fired respectively when the `responsecache:clear` is started and finished. 469 | 470 | ### Creating a Replacer 471 | 472 | To replace cached content by dynamic content, you can create a replacer. 473 | By default we add a `CsrfTokenReplacer` in the config file. 474 | 475 | You can create your own replacers by implementing the `Spatie\ResponseCache\Replacers\Replacer` interface. Let's take a look at the interface: 476 | 477 | ```php 478 | interface Replacer 479 | { 480 | /* 481 | * Prepare the initial response before it gets cached. 482 | * 483 | * For example: replace a generated csrf_token by '' that you can 484 | * replace with its dynamic counterpart when the cached response is returned. 485 | */ 486 | public function prepareResponseToCache(Response $response): void; 487 | 488 | /* 489 | * Replace any data you want in the cached response before it gets 490 | * sent to the browser. 491 | * 492 | * For example: replace '' by a call to csrf_token() 493 | */ 494 | public function replaceInCachedResponse(Response $response): void; 495 | } 496 | ``` 497 | 498 | Afterwards you can define your replacer in the `responsecache.php` config file: 499 | 500 | ``` 501 | /* 502 | * Here you may define replacers that dynamically replace content from the response. 503 | * Each replacer must implement the Replacer interface. 504 | */ 505 | 'replacers' => [ 506 | \Spatie\ResponseCache\Replacers\CsrfTokenReplacer::class, 507 | ], 508 | ``` 509 | 510 | ### Customizing the serializer 511 | 512 | A serializer is responsible from serializing a response so it can be stored in the cache. It is also responsible for rebuilding the response from the cache. 513 | 514 | The default serializer `Spatie\ResponseCache\Serializer\DefaultSerializer` will just work in most cases. 515 | 516 | If you have some special serialization needs you can specify a custom serializer in the `serializer` key of the config file. Any class that implements `Spatie\ResponseCache\Serializers\Serializer` can be used. This is how that interface looks like: 517 | 518 | ```php 519 | namespace Spatie\ResponseCache\Serializers; 520 | 521 | use Symfony\Component\HttpFoundation\Response; 522 | 523 | interface Serializer 524 | { 525 | public function serialize(Response $response): string; 526 | 527 | public function unserialize(string $serializedResponse): Response; 528 | } 529 | ``` 530 | ## Testing 531 | 532 | You can run the tests with: 533 | ``` bash 534 | composer test 535 | ``` 536 | 537 | ## Alternatives 538 | 539 | - [Barry Vd. Heuvel](https://twitter.com/barryvdh) made [a package that caches responses by leveraging HttpCache](https://github.com/barryvdh/laravel-httpcache). 540 | - [Joseph Silber](https://twitter.com/joseph_silber) created [Laravel Page Cache](https://github.com/JosephSilber/page-cache) that can write its cache to disk and let Nginx read them, so PHP doesn't even have to start up anymore. 541 | 542 | ## Changelog 543 | 544 | Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. 545 | 546 | ## Contributing 547 | 548 | Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details. 549 | 550 | ## Security 551 | 552 | If you've found a bug regarding security please mail [security@spatie.be](mailto:security@spatie.be) instead of using the issue tracker. 553 | 554 | ## Credits 555 | 556 | - [Freek Van der Herten](https://github.com/freekmurze) 557 | - [All Contributors](../../contributors) 558 | 559 | Special thanks to [Caneco](https://twitter.com/caneco) for the original logo. 560 | 561 | ## License 562 | 563 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 564 | --------------------------------------------------------------------------------