├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── composer.lock ├── config └── model-cache.php ├── example ├── User.php └── UsersController.php ├── src ├── CacheableBuilder.php ├── CachingBelongsToMany.php ├── Console │ └── Commands │ │ └── ClearModelCacheCommand.php ├── HasCachedQueries.php ├── ModelCacheServiceProvider.php └── ModelRelationships.php └── tests └── .gitignore /.gitignore: -------------------------------------------------------------------------------- 1 | # Laravel specific 2 | /vendor 3 | 4 | # IDEs and editors 5 | /.fleet 6 | /.idea 7 | /.vscode 8 | /.phpstorm.meta.php 9 | /_ide_helper.php 10 | /_ide_helper_models.php 11 | 12 | # OS specific 13 | .DS_Store 14 | Thumbs.db 15 | 16 | 17 | # Keep these directories 18 | !.gitignore 19 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-model-cache` will be documented in this file. 4 | 5 | ## [1.1.2] - 2025-05-21 6 | 7 | ### Fixed 8 | - Fixed implementation of the `ModelRelationships` trait to properly handle BelongsToMany operations. Replaced the event-based approach (which was relying on non-existent Laravel events) with a custom BelongsToMany relationship class that flushes the cache after attach, detach, sync, syncWithoutDetaching, and updateExistingPivot operations. 9 | - Updated `CachingBelongsToMany` class to properly extend Laravel's BelongsToMany class and maintain the relationship contract. This resolves the "must return a relationship instance" error when accessing relationship properties after operations like attach() and detach(). 10 | 11 | ## [1.1.1] - 2025-05-19 12 | 13 | ### Fixed 14 | - Fixed implementation of configuration for enabling or disabling cache: The `model-cache.enabled` configuration parameter is now properly checked in the HasCachedQueries trait, ensuring that when cache is disabled via configuration, the standard Eloquent builder is used instead of the cache-enabled version. 15 | 16 | 17 | ## [1.1.0] - 2025-05-13 18 | 19 | ### Added 20 | - Added support for custom cache prefix per model via `$cachePrefix` property 21 | - Added `ModelRelationships` trait to support cache invalidation for Eloquent relationship operations 22 | - Support for flushing cache on belongsToMany relationship events (saved, attached, detached, synced, updated) 23 | - New helper methods for relationship operations with automatic cache flushing: `syncRelationshipAndFlushCache()`, `attachRelationshipAndFlushCache()`, `detachRelationshipAndFlushCache()` 24 | - Debug logging for relationship-triggered cache flush operations when debug_mode is enabled 25 | 26 | ### Fixed 27 | - Fixed issue with custom cache minutes (`$cacheMinutes`) definition at the model level 28 | - Improved cache invalidation when working with Eloquent relationships 29 | - Better handling of model relationship events to ensure cache consistency 30 | 31 | 32 | 33 | ## [1.0.1] - 2025-05-04 34 | 35 | ### Fixed 36 | 37 | - Add debug_mode config check for logger usage in model cache: This ensures logging of cache flush operations only 38 | occurs when debug_mode is explicitly enabled in the configuration. It reduces unnecessary log entries in production 39 | environments while retaining detailed logs for debugging purposes. 40 | 41 | ## [1.0.0] - 2025-05-01 42 | 43 | ### Added 44 | - Initial implementation of Eloquent model caching system 45 | - `HasCachedQueries` trait to enable caching on any model 46 | - Transparent integration with Laravel's query builder 47 | - Explicit methods for cache control (`getFromCache()`, `firstFromCache()`) 48 | - Automatic cache invalidation when models are created, updated, deleted, or restored 49 | - Full cache tag support 50 | - `mcache:flush` Artisan command for manual cache clearing 51 | - Customizable configuration for cache duration, key prefix, and cache store 52 | - Compatibility with Laravel 8.x, 9.x, 10.x, 11.x, and 12.x 53 | - Support for PHP 7.4, 8.0, 8.1, 8.2, and 8.3 54 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Laravel Model Cache 2 | 3 | Thank you for considering contributing to Laravel Model Cache! This document outlines the guidelines for contributing to 4 | this project. 5 | 6 | ## About Laravel Model Cache 7 | 8 | Laravel Model Cache is a package that provides efficient caching for Eloquent model queries. It helps improve 9 | application performance by reducing database queries through intelligent caching mechanisms. The package uses Laravel's 10 | built-in cache system with tags to efficiently manage cached queries for Eloquent models. 11 | 12 | ## Package Architecture 13 | 14 | The package consists of the following key components: 15 | 16 | 1. `HasCachedQueries` trait - Add to your Eloquent models to enable caching 17 | 2. `CacheableBuilder` - Extends Eloquent's query builder to add caching functionality 18 | 3. `ModelCacheServiceProvider` - Registers the package with Laravel 19 | 4. Console commands for cache management 20 | 21 | ## Code of Conduct 22 | 23 | By participating in this project, you agree to abide by 24 | the [Laravel Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct). 25 | 26 | ## Setup 27 | 28 | 1. Fork the repository 29 | 2. Clone your fork: `git clone https://github.com/ymigval/laravel-model-cache.git` 30 | 3. Add the upstream repository: `git remote add upstream https://github.com/ymigval/laravel-model-cache.git` 31 | 4. Create a branch for your changes: `git checkout -b feature/your-feature-name` 32 | 5. Install dependencies: `composer install` 33 | 34 | ## Development Environment 35 | 36 | ### Requirements 37 | 38 | - PHP ^7.4, ^8.0, ^8.1, ^8.2, or ^8.3 39 | - Laravel 8.x, 9.x, 10.x, 11.x, or 12.x 40 | - Composer 41 | 42 | ### Installation for Development 43 | 44 | After cloning the repository and installing dependencies, you can set up the package for development: 45 | 46 | 1. Create a Laravel application for testing: `composer create-project laravel/laravel test-app` 47 | 2. In your test application's `composer.json`, add a repository pointing to your local clone: 48 | 49 | ```json 50 | { 51 | "repositories": [ 52 | { 53 | "type": "path", 54 | "url": "../path/to/your/laravel-model-cache" 55 | } 56 | ] 57 | } 58 | ``` 59 | 60 | 3. Require the package in development mode: `composer require ymigval/laravel-model-cache:@dev` 61 | 62 | ## Development Workflow 63 | 64 | ### Coding Standards 65 | 66 | This project follows the [PSR-12](https://www.php-fig.org/psr/psr-12/) coding standard and 67 | the [Laravel coding style](https://laravel.com/docs/contributions#coding-style). 68 | 69 | To ensure your code follows these standards, run: 70 | 71 | ```bash 72 | composer run-script check-style 73 | ``` 74 | 75 | To automatically fix most style issues, run: 76 | 77 | ``` bash 78 | composer run-script fix-style 79 | ``` 80 | 81 | ### Testing 82 | 83 | This package has a comprehensive test suite. Before submitting your changes, make sure all tests pass: 84 | 85 | ``` bash 86 | composer test 87 | ``` 88 | 89 | ### Adding New Features 90 | 91 | When adding new features, please follow these guidelines: 92 | 93 | 1. Create a new branch for your feature: `git checkout -b feature/your-feature-name` 94 | 2. Update the tests to cover your new feature 95 | 3. Update documentation (README.md, PHPDoc comments, etc.) 96 | 4. Submit a pull request 97 | 98 | ### Bug Fixes 99 | 100 | When fixing bugs, please follow these guidelines: 101 | 102 | 1. Create a new branch for your fix: `git checkout -b fix/your-fix-name` 103 | 2. Add a test that reproduces the bug 104 | 3. Fix the bug 105 | 4. Make sure all tests pass 106 | 5. Submit a pull request 107 | 108 | ## Pull Request Process 109 | 110 | 1. Ensure your code follows the coding standards 111 | 2. Update documentation as necessary 112 | 3. Add or update tests as necessary 113 | 4. The pull request should target the `main` branch 114 | 5. Make sure CI tests pass 115 | 6. Wait for a maintainer to review your PR 116 | 117 | ## Release Process 118 | 119 | The maintainers will handle the release process. Generally, releases follow these steps: 120 | 121 | 1. Update version number in composer.json 122 | 2. Update CHANGELOG.md with changes since the last release 123 | 3. Create a new tag for the release 124 | 4. Push the tag to GitHub 125 | 5. Create a new release on GitHub 126 | 127 | ## Support 128 | 129 | If you have questions about contributing to Laravel Model Cache, please: 130 | 131 | 1. Check existing issues to see if your question has already been answered 132 | 2. Open a new issue if your question hasn't been addressed 133 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | Copyright (c) Yordan 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 Model Cache 2 | 3 | A simple and efficient caching solution for Laravel Eloquent models. 4 | 5 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/ymigval/laravel-model-cache.svg?style=flat-square)](https://packagist.org/packages/ymigval/laravel-model-cache) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/ymigval/laravel-model-cache.svg?style=flat-square)](https://packagist.org/packages/ymigval/laravel-model-cache) 7 | [![License](https://img.shields.io/packagist/l/ymigval/laravel-model-cache.svg?style=flat-square)](LICENSE.md) 8 | 9 | ## Introduction 10 | 11 | Laravel Model Cache provides a powerful way to cache your Eloquent query results. It transparently integrates with 12 | Laravel's query builder system to automatically cache all query results, with zero changes to your existing query 13 | syntax. The package intelligently handles cache invalidation when models are created, updated, deleted, or restored. 14 | 15 | ## Features 16 | 17 | - **Deep Integration**: Replaces Laravel's query builder with a cache-aware version 18 | - **Transparent Caching**: All query methods (`get()`, `first()`, etc.) automatically use cache 19 | - **Explicit Control**: Additional methods for when you want to be explicit about caching 20 | - **Automatic Invalidation**: Cache is cleared when models are created, updated, deleted, or restored 21 | - **Full Tag Support**: Works with Laravel's built-in cache tagging system 22 | - **Performance Optimized**: Dramatically reduces database queries for read-heavy applications 23 | - **Drop-in Solution**: Compatible with existing Laravel query builder syntax 24 | - **Highly Configurable**: Easy to customize cache duration, store, and behavior 25 | 26 | ## Requirements 27 | 28 | - PHP ^7.4, ^8.0, ^8.1, ^8.2, or ^8.3 29 | - Laravel 8.x, 9.x, 10.x, 11.x, or 12.x 30 | 31 | ## Installation 32 | 33 | Install the package via Composer: 34 | 35 | ```shell script 36 | composer require ymigval/laravel-model-cache 37 | ``` 38 | 39 | ## Publish the Configuration File (optional) 40 | 41 | To customize the package configuration, publish the configuration file: 42 | 43 | ```shell script 44 | php artisan vendor:publish --provider="YMigVal\LaravelModelCache\ModelCacheServiceProvider" --tag="config" 45 | ``` 46 | 47 | This creates with the following customizable options: `config/model-cache.php` 48 | 49 | - `cache_duration`: Default cache TTL in minutes (default: 60) 50 | - `cache_key_prefix`: Prefix for all cache keys (default: 'model_cache_') 51 | - `cache_store`: Specific cache store to use for model caching 52 | - `enabled`: Global toggle to enable/disable the cache functionality 53 | 54 | ## Service Provider Registration 55 | 56 | The package comes with Laravel package auto-discovery for Laravel 5.5+. For older versions, register the service 57 | provider in : `config/app.php` 58 | 59 | ```php 60 | 'providers' => [ 61 | // Other service providers... 62 | YMigVal\LaravelModelCache\ModelCacheServiceProvider::class, 63 | ], 64 | ``` 65 | 66 | ## Cache Driver Configuration 67 | 68 | ### Supported Cache Drivers 69 | 70 | This package uses Laravel's tagging system for cache, so you must use a cache driver that supports tags: 71 | 72 | 1. **Redis** (Recommended) 73 | - Offers excellent performance and tag support 74 | - Configure in `.env`: 75 | 76 | ``` 77 | CACHE_STORE=redis 78 | 79 | REDIS_CLIENT=phpredis # or predis 80 | REDIS_HOST=127.0.0.1 81 | REDIS_PASSWORD=null 82 | REDIS_PORT=6379 83 | ``` 84 | 85 | 2. **Memcached** 86 | - Another good option with tag support 87 | - Configure in `.env`: 88 | 89 | ``` 90 | CACHE_STORE=memcached 91 | MEMCACHED_HOST=127.0.0.1 92 | MEMCACHED_PORT=11211 93 | ``` 94 | 95 | 3. **Database** 96 | - Supports tags but slower than Redis/Memcached 97 | - Requires cache table creation: 98 | 99 | ``` 100 | php artisan cache:table 101 | php artisan migrate 102 | ``` 103 | 104 | - Configure in `.env`: 105 | 106 | ``` 107 | CACHE_STORE=database 108 | ``` 109 | 110 | 4. **File** and **Array** drivers do not support tags, but the package includes fallback mechanisms that make them 111 | compatible: 112 | - These drivers will work for basic caching functionality 113 | - When using these drivers, cache invalidation will clear the entire cache rather than just specific model entries 114 | - While not as efficient as tag-supporting drivers, they can be used in development or when other drivers are not 115 | available 116 | - Configure in `.env`: 117 | 118 | ``` 119 | CACHE_STORE=file 120 | ``` 121 | 122 | ## Optional Configuration in Config File 123 | 124 | The file allows you to adjust: `config/model-cache.php` 125 | 126 | - Default cache Time-To-Live (TTL) 127 | - Globally enable/disable caching 128 | - Prefix for all cache keys 129 | - Specific cache driver for model caching 130 | 131 | ### Cache Store Selection 132 | 133 | You can specify which cache store to use specifically for model caching in the config file: 134 | 135 | ``` php 136 | // config/model-cache.php 137 | 'cache_store' => env('MODEL_CACHE_STORE', 'redis'), 138 | ``` 139 | 140 | This allows you to use a different cache store for models than your application's default cache store. 141 | 142 | ## Implementation in Models 143 | 144 | Add the trait to any Eloquent model you want to cache: 145 | 146 | ```php 147 | get(); 191 | 192 | // This also uses the cache with default duration 193 | $post = Post::where('id', 1)->first(); 194 | 195 | // Add custom cache duration 196 | $posts = Post::where('status', 'active')->remember(60)->get(); 197 | ``` 198 | 199 | ### 2. Explicit Caching Methods 200 | 201 | For more explicit control and code readability, use the dedicated caching methods: 202 | 203 | ```php 204 | // Explicitly get results from cache (or store in cache if not present) 205 | $posts = Post::where('published', true)->getFromCache(); 206 | 207 | // Explicitly get first result from cache 208 | $post = Post::where('id', 1)->firstFromCache(); 209 | 210 | // Set custom cache duration for a specific query 211 | $posts = Post::where('status', 'active')->remember(30)->getFromCache(); 212 | ``` 213 | 214 | Both approaches produce the same result - they check the cache first and only hit the database if needed. 215 | 216 | ## Choosing Between Implicit and Explicit Caching 217 | 218 | Both approaches have advantages depending on your use case: 219 | 220 | | Approach | When to Use | Benefits | 221 | |---------------------------------|---------------------------------------------|--------------------------------------------------------------------------------------------------| 222 | | **Implicit** (`get()`) | For seamless integration into existing code | - No code changes needed
- Transparent performance boost
- Works with existing code | 223 | | **Explicit** (`getFromCache()`) | When you want caching to be obvious | - Self-documenting code
- Clearer for team members
- Highlights performance considerations | 224 | 225 | ### Performance Comparison 226 | 227 | There is no performance difference between the two approaches - both implementations use the same underlying caching 228 | mechanism. The choice is purely about code readability and developer preference. 229 | 230 | ## Advanced Caching Strategies 231 | 232 | ### Using Query Scopes with Caching 233 | 234 | Combine Laravel scopes with cache settings for reusable cached queries: 235 | 236 | ```php 237 | // In your model 238 | public function scopePopular($query) 239 | { 240 | return $query->where('views', '>', 1000) 241 | ->orderBy('views', 'desc') 242 | ->remember(60); // Cache popular posts for 1 hour 243 | } 244 | 245 | // In your controller - both work the same 246 | $posts = Post::popular()->get(); // Implicit 247 | $posts = Post::popular()->getFromCache(); // Explicit 248 | ``` 249 | 250 | ### Conditional Caching 251 | 252 | Dynamically decide whether to cache based on conditions: 253 | 254 | ```php 255 | $minutes = $user->isAdmin() ? 5 : 60; // Less cache time for admins who need fresh data 256 | $posts = Post::latest()->remember($minutes)->get(); 257 | ``` 258 | 259 | ### Selective Caching with Relations 260 | 261 | Cache only specific relations: 262 | 263 | ```php 264 | // Cache the posts but load comments fresh every time 265 | $posts = Post::with(['comments' => function($query) { 266 | $query->withoutCache(); // Skip cache for this relation 267 | }])->remember(30)->get(); 268 | ``` 269 | 270 | ## Manually Clearing Cache Using Console Commands 271 | 272 | Laravel Model Cache includes built-in Artisan commands to easily clear the cache for your models from the command line. 273 | This feature is especially useful when you need to manually invalidate cache during deployments or when troubleshooting. 274 | 275 | ### Available Cache Clearing Commands 276 | 277 | The package registers the following Artisan command: 278 | 279 | ``` shell 280 | php artisan mcache:flush {model?} 281 | ``` 282 | 283 | The `{model?}` parameter is optional. When provided, it should be the fully qualified class name of the model you want 284 | to clear the cache for. 285 | 286 | ## Command Usage Examples 287 | 288 | ### Clear Cache for a Specific Model 289 | 290 | To clear the cache for a specific model, provide the fully qualified class name: 291 | 292 | ``` shell 293 | # Clear cache for User model 294 | php artisan mcache:flush "App\Models\User" 295 | 296 | # Clear cache for Product model 297 | php artisan mcache:flush "App\Models\Product" 298 | ``` 299 | 300 | The command will: 301 | 302 | 1. Instantiate the model class 303 | 2. Call the `flushModelCache()` method or clear the cache manually 304 | 3. Display a confirmation message when complete 305 | 306 | ### Clear Cache for All Models 307 | 308 | To clear the cache for all models at once, run the command without any arguments: 309 | 310 | ``` shell 311 | php artisan mcache:flush 312 | ``` 313 | 314 | This will clear all cache entries tagged with 'model_cache', effectively clearing the cache for all models that use the 315 | package. 316 | 317 | ## How the Command Works 318 | 319 | 1. **Tag-Based Clearing** 320 | - The command uses cache tags to efficiently clear only relevant cache entries 321 | - Tags are structured as ['model_cache', ModelClassName, TableName] 322 | - This is efficient as it only removes cache related to the specified model 323 | 324 | 2. **Fallback for Non-Tag-Supporting Drivers** 325 | - For cache drivers that don't support tags (File, Database), the command falls back to clearing all cache 326 | - The command will display a warning and ask for confirmation before proceeding with a full cache clear 327 | - This is necessary because without tag support, it's not possible to selectively clear only model-related cache 328 | 329 | ## Programmatic Cache Clearing 330 | 331 | You can also clear the cache programmatically in your application code: 332 | 333 | ``` php 334 | // Clear cache for a single model instance 335 | $user = User::find(1); 336 | $user->flushCache(); 337 | 338 | // Clear cache for an entire model class 339 | User::flushModelCache(); 340 | ``` 341 | 342 | ## When to Use Cache Clearing Commands 343 | 344 | Consider manually clearing cache in these situations: 345 | 346 | 1. **Deployments**: After deploying new code that might make cached data obsolete 347 | 2. **Data Imports**: After bulk importing data that bypasses model events 348 | 3. **Schema Changes**: After changing database schema that affects model structures 349 | 4. **Debugging**: When troubleshooting issues that might be related to stale cache 350 | 351 | ## Performance Considerations 352 | 353 | - Consider setting different cache durations for different models based on how frequently they change 354 | - Use cache tags wisely to avoid invalidating too much cache at once 355 | - For high-traffic applications, consider implementing a cache warming strategy 356 | - When dealing with very large datasets, consider paginating results to reduce cache size 357 | - Monitor your cache storage usage regularly when implementing on large tables 358 | 359 | ## Using ModelRelationships Trait 360 | 361 | The `ModelRelationships` trait provides enhanced support for cache invalidation when working with Eloquent 362 | relationships. It automatically flushes the cache when relationship operations (like attaching, detaching, or syncing 363 | pivot records) are performed. 364 | 365 | ### Implementation in Models 366 | 367 | Add both traits to your model: 368 | 369 | ```php 370 | belongsToMany(Tag::class); 386 | } 387 | } 388 | ``` 389 | 390 | ### Using Helper Methods for Relationships 391 | 392 | The trait provides convenient methods to manipulate relationships while ensuring the cache is properly invalidated: 393 | 394 | ```php 395 | // Sync a belongsToMany relationship and flush cache 396 | $post->syncRelationshipAndFlushCache('tags', [1, 2, 3]); 397 | 398 | // Attach records to a belongsToMany relationship and flush cache 399 | $post->attachRelationshipAndFlushCache('tags', [4, 5], ['added_by' => 'admin']); 400 | 401 | // Detach records from a belongsToMany relationship and flush cache 402 | $post->detachRelationshipAndFlushCache('tags', [1, 3]); 403 | ``` 404 | 405 | ### Automatic Cache Invalidation 406 | 407 | The trait also automatically flushes the cache when Laravel's standard relationship methods are 408 | used by providing a custom BelongsToMany relationship implementation: 409 | 410 | 411 | ```php 412 | // These operations will automatically flush the cache 413 | $post->tags()->attach(1); 414 | $post->tags()->detach([2, 3]); 415 | $post->tags()->sync([1, 4, 5]); 416 | $post->tags()->updateExistingPivot(1, ['featured' => true]); 417 | $post->tags()->syncWithoutDetaching([1, 5]); 418 | ``` 419 | 420 | This ensures that your cached queries always reflect the current state of your model relationships. 421 | 422 | ## Troubleshooting 423 | 424 | ### Cache Not Being Used 425 | 426 | If your queries are not being cached: 427 | 428 | 1. Verify that your model correctly uses the `HasCachedQueries` trait 429 | 2. Check that you're using a compatible cache driver (Redis, Memcached, Database) 430 | 3. Make sure that `model-cache.enabled` is set to `true` in your configuration 431 | 4. Temporarily add logging to verify if cache hits or misses are occurring: 432 | ```php 433 | // Add this in your controller 434 | if (Cache::has($cacheKey)) { 435 | Log::info('Cache hit for key: ' . $cacheKey); 436 | } else { 437 | Log::info('Cache miss for key: ' . $cacheKey); 438 | } 439 | ``` 440 | 441 | ### Cache Not Being Invalidated 442 | 443 | If old data persists in the cache after updates: 444 | 445 | 1. Make sure your model is correctly firing create/update/delete events 446 | 2. Check if you're using query builder methods that bypass model events (`DB::table()->update()`) 447 | 3. Try manually clearing the cache to test: `YourModel::flushModelCache()` 448 | 4. Verify that your cache driver correctly supports tags if you're using them 449 | 450 | ## Frequently Asked Questions 451 | 452 | ### Q: Why use this package instead of Laravel's built-in caching? 453 | 454 | A: This package provides automatic cache invalidation, tag-based cache management, and seamless integration with 455 | Eloquent with minimal code changes. 456 | 457 | ### Q: Does this work with soft deleted models? 458 | 459 | A: Yes, the package respects Laravel's soft delete functionality and will cache accordingly. 460 | 461 | ### Q: What's the difference between `get()` and `getFromCache()`? 462 | 463 | A: Functionally they are identical when using this package - both check cache first. The difference is syntax preference 464 | and code readability. 465 | 466 | ### Q: Can I still use regular database queries when needed? 467 | 468 | A: Yes, you can bypass the cache using: 469 | 470 | ```php 471 | $freshData = YourModel::withoutCache()->get(); 472 | ``` 473 | 474 | ### Q: Does this work with relationships? 475 | 476 | A: Yes, caching works with eager-loaded relationships and regular relationship queries. The package correctly generates unique cache keys for queries with different eager-loaded relationships, ensuring that `Model::get()` and `Model::with(['relation'])->get()` use different cache entries. 477 | 478 | ### Q: How can I troubleshoot cache issues? 479 | 480 | A: You can enable debug mode in your configuration file to see detailed logs about cache keys being generated: 481 | 482 | ## Migrating from Other Caching Solutions 483 | 484 | ### From Manual Laravel Cache 485 | 486 | If you're currently using Laravel's Cache facade manually: 487 | 488 | ```php 489 | // Old approach with manual caching 490 | $cacheKey = 'posts_' . md5($query); 491 | $posts = Cache::remember($cacheKey, 60, function() use ($query) { 492 | return Post::where(...)->get(); 493 | }); 494 | 495 | // New approach with this package 496 | $posts = Post::where(...)->remember(60)->get(); 497 | ``` 498 | 499 | ### From Other Cache Packages 500 | 501 | If you're using another caching package: 502 | 503 | 1. Replace the other package's trait with `HasCachedQueries` 504 | 2. Remove manual cache key generation code 505 | 3. Replace custom cache retrieval methods with standard Eloquent methods or the explicit `getFromCache()` methods 506 | 4. Remove manual cache invalidation code (this package handles it automatically) -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ymigval/laravel-model-cache", 3 | "description": "Laravel package for caching Eloquent model queries", 4 | "homepage": "https://github.com/ymigval/laravel-model-cache", 5 | "keywords": [ 6 | "redis", 7 | "php", 8 | "caching", 9 | "memcached", 10 | "laravel", 11 | "performance", 12 | "eloquent", 13 | "orm", 14 | "model", 15 | "cache", 16 | "laravel-package", 17 | "database-caching", 18 | "laravel-eloquent", 19 | "query-cacher", 20 | "model-cache" 21 | ], 22 | "type": "library", 23 | "license": "MIT", 24 | "authors": [ 25 | { 26 | "name": "Yordan", 27 | "email": "banatube@gmail.com" 28 | } 29 | ], 30 | "minimum-stability": "dev", 31 | "prefer-stable": true, 32 | "require": { 33 | "php": "^7.4|^8.0|^8.1|^8.2|^8.3", 34 | "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0", 35 | "illuminate/database": "^8.0|^9.0|^10.0|^11.0|^12.0", 36 | "illuminate/cache": "^8.0|^9.0|^10.0|^11.0|^12.0", 37 | "ext-json": "*" 38 | }, 39 | "require-dev": { 40 | "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0", 41 | "phpunit/phpunit": "^9.0|^10.0|^11.0", 42 | "friendsofphp/php-cs-fixer": "^3.0" 43 | }, 44 | "autoload": { 45 | "psr-4": { 46 | "YMigVal\\LaravelModelCache\\": "src/" 47 | } 48 | }, 49 | "autoload-dev": { 50 | "psr-4": { 51 | "YMigVal\\LaravelModelCache\\Tests\\": "tests/" 52 | } 53 | }, 54 | "scripts": { 55 | "test": "vendor/bin/phpunit", 56 | "check-style": "vendor/bin/php-cs-fixer fix --dry-run", 57 | "fix-style": "vendor/bin/php-cs-fixer fix" 58 | }, 59 | "extra": { 60 | "laravel": { 61 | "providers": [ 62 | "YMigVal\\LaravelModelCache\\ModelCacheServiceProvider" 63 | ] 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/model-cache.php: -------------------------------------------------------------------------------- 1 | 60, 13 | 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Cache Key Prefix 17 | |-------------------------------------------------------------------------- 18 | | 19 | | This prefix will be used for all cache keys to avoid collisions. 20 | | 21 | */ 22 | 'cache_key_prefix' => 'model_cache_', 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Cache Store 27 | |-------------------------------------------------------------------------- 28 | | 29 | | This option controls the cache store that gets used for storing 30 | | and retrieving queries. Use env('MODEL_CACHE_STORE') to specify 31 | | a different store than your main application cache. 32 | | 33 | | Note: For tag support, use Redis or Memcached drivers. 34 | | 35 | */ 36 | 'cache_store' => env('MODEL_CACHE_STORE', null), 37 | 38 | /* 39 | |-------------------------------------------------------------------------- 40 | | Enable Query Caching 41 | |-------------------------------------------------------------------------- 42 | | 43 | | This option provides an easy way to globally enable/disable query caching. 44 | | 45 | */ 46 | 'enabled' => env('MODEL_CACHE_ENABLED', true), 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Debug Mode 51 | |-------------------------------------------------------------------------- 52 | | 53 | | When enabled, this will log detailed information about cache keys and 54 | | queries being cached. Useful for troubleshooting cache-related issues. 55 | | 56 | */ 57 | 'debug_mode' => env('MODEL_CACHE_DEBUG', false), 58 | ]; 59 | -------------------------------------------------------------------------------- /example/User.php: -------------------------------------------------------------------------------- 1 | 'datetime', 39 | ]; 40 | 41 | /** 42 | * Example of using the cached queries in a scope. 43 | * 44 | * @param \Illuminate\Database\Eloquent\Builder $query 45 | * @return \Illuminate\Database\Eloquent\Builder 46 | */ 47 | public function scopeActive($query) 48 | { 49 | // The query results will be cached for 30 minutes 50 | return $query->where('active', true)->remember(30); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /example/UsersController.php: -------------------------------------------------------------------------------- 1 | getFromCache(); 19 | 20 | return view('users.index', compact('users')); 21 | } 22 | 23 | /** 24 | * Display a listing of all users with custom cache duration. 25 | * 26 | * @return \Illuminate\Http\Response 27 | */ 28 | public function all() 29 | { 30 | // Cache the results for 2 hours 31 | $users = User::remember(120)->getFromCache(); 32 | 33 | return view('users.all', compact('users')); 34 | } 35 | 36 | /** 37 | * Find a specific user by email, using cache. 38 | * 39 | * @param Request $request 40 | * @return \Illuminate\Http\Response 41 | */ 42 | public function findByEmail(Request $request) 43 | { 44 | $email = $request->input('email'); 45 | 46 | // This will use the cached query result or store it if not exists 47 | $user = User::where('email', $email)->remember()->firstFromCache(); 48 | 49 | if (!$user) { 50 | return redirect()->back()->with('error', 'User not found.'); 51 | } 52 | 53 | return view('users.show', compact('user')); 54 | } 55 | 56 | /** 57 | * Update a user, which will automatically invalidate cache. 58 | * 59 | * @param Request $request 60 | * @param User $user 61 | * @return \Illuminate\Http\Response 62 | */ 63 | public function update(Request $request, User $user) 64 | { 65 | $user->update($request->validated()); 66 | 67 | // No need to manually invalidate cache - the HasCachedQueries trait 68 | // handles this automatically when the model is updated 69 | 70 | return redirect()->route('users.show', $user)->with('success', 'User updated.'); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/CacheableBuilder.php: -------------------------------------------------------------------------------- 1 | cacheMinutes = $cacheMinutes; 32 | $this->cachePrefix = $cachePrefix; 33 | parent::__construct($builder); 34 | } 35 | 36 | 37 | /** 38 | * Create a new model instance and store it in the database. 39 | * 40 | * @param array $attributes 41 | * @return \Illuminate\Database\Eloquent\Model|$this 42 | */ 43 | public function create(array $attributes = []) 44 | { 45 | $model = parent::create($attributes); 46 | 47 | // Flush cache after creating a model 48 | if ($model && method_exists($model, 'flushModelCache')) { 49 | $model->flushModelCache(); 50 | } else { 51 | $this->flushQueryCache(); 52 | } 53 | 54 | return $model; 55 | } 56 | 57 | /** 58 | * Create a new instance of the model being queried. 59 | * 60 | * @param array $attributes 61 | * @return \Illuminate\Database\Eloquent\Model|static 62 | */ 63 | public function make(array $attributes = []) 64 | { 65 | return parent::make($attributes); 66 | } 67 | 68 | /** 69 | * Create a new model instance and store it in the database without mass assignment protection. 70 | * 71 | * @param array $attributes 72 | * @return \Illuminate\Database\Eloquent\Model|$this 73 | */ 74 | public function forceCreate(array $attributes) 75 | { 76 | $model = parent::forceCreate($attributes); 77 | 78 | // Flush cache after force creating a model 79 | if ($model && method_exists($model, 'flushModelCache')) { 80 | $model->flushModelCache(); 81 | } else { 82 | $this->flushQueryCache(); 83 | } 84 | 85 | return $model; 86 | } 87 | 88 | /** 89 | * Get the first record matching the attributes or create it. 90 | * 91 | * @param array $attributes 92 | * @param array $values 93 | * @return \Illuminate\Database\Eloquent\Model|static 94 | */ 95 | public function firstOrCreate(array $attributes = [], array $values = []) 96 | { 97 | $model = parent::firstOrCreate($attributes, $values); 98 | 99 | // Flush cache if model was created (doesn't exist before) 100 | if ($model->wasRecentlyCreated && method_exists($model, 'flushModelCache')) { 101 | $model->flushModelCache(); 102 | } elseif ($model->wasRecentlyCreated) { 103 | $this->flushQueryCache(); 104 | } 105 | 106 | return $model; 107 | } 108 | 109 | /** 110 | * Get the first record matching the attributes or instantiate it. 111 | * 112 | * @param array $attributes 113 | * @param array $values 114 | * @return \Illuminate\Database\Eloquent\Model|static 115 | */ 116 | public function firstOrNew(array $attributes = [], array $values = []) 117 | { 118 | return parent::firstOrNew($attributes, $values); 119 | } 120 | 121 | /** 122 | * Save a new model and return the instance. 123 | * 124 | * @param array $attributes 125 | * @return \Illuminate\Database\Eloquent\Model|$this 126 | */ 127 | public function save(array $attributes = []) 128 | { 129 | $instance = $this->newModelInstance($attributes); 130 | 131 | $instance->save(); 132 | 133 | // Flush cache after saving 134 | if (method_exists($instance, 'flushModelCache')) { 135 | $instance->flushModelCache(); 136 | } else { 137 | $this->flushQueryCache(); 138 | } 139 | 140 | return $instance; 141 | } 142 | 143 | /** 144 | * Save a new model without mass assignment protection and return the instance. 145 | * 146 | * @param array $attributes 147 | * @return \Illuminate\Database\Eloquent\Model|$this 148 | */ 149 | public function forceSave(array $attributes = []) 150 | { 151 | $instance = $this->newModelInstance(); 152 | $instance->forceFill($attributes)->save(); 153 | 154 | // Flush cache after saving 155 | if (method_exists($instance, 'flushModelCache')) { 156 | $instance->flushModelCache(); 157 | } else { 158 | $this->flushQueryCache(); 159 | } 160 | 161 | return $instance; 162 | } 163 | 164 | /** 165 | * Save a collection of models to the database. 166 | * 167 | * @param array|\Illuminate\Support\Collection $models 168 | * @return array|\Illuminate\Support\Collection 169 | */ 170 | public function saveMany($models) 171 | { 172 | foreach ($models as $model) { 173 | $model->save(); 174 | } 175 | 176 | // Flush cache after saving multiple models 177 | if (count($models) > 0) { 178 | $model = $models[0]; 179 | if (method_exists($model, 'flushModelCache')) { 180 | $model->flushModelCache(); 181 | } else { 182 | $this->flushQueryCache(); 183 | } 184 | } 185 | 186 | return $models; 187 | } 188 | 189 | /** 190 | * Create multiple instances of the model. 191 | * 192 | * @param array $records 193 | * @return \Illuminate\Database\Eloquent\Collection 194 | */ 195 | public function createMany(array $records) 196 | { 197 | $instances = new Collection(); 198 | 199 | foreach ($records as $record) { 200 | $instances->push($this->create($record)); 201 | } 202 | 203 | return $instances; 204 | } 205 | 206 | /** 207 | * Update records in the database without raising any events. 208 | * 209 | * @param array $values 210 | * @return int 211 | */ 212 | public function updateQuietly(array $values) 213 | { 214 | $model = $this->model; 215 | 216 | $result = $model->withoutEvents(function () use ($values) { 217 | return $this->update($values); 218 | }); 219 | 220 | // Still flush cache even though events aren't fired 221 | if ($result && method_exists($model, 'flushModelCache')) { 222 | $model->flushModelCache(); 223 | } elseif ($result) { 224 | $this->flushQueryCache(); 225 | } 226 | 227 | return $result; 228 | } 229 | 230 | /** 231 | * Delete records from the database without raising any events. 232 | * 233 | * @return mixed 234 | */ 235 | public function deleteQuietly() 236 | { 237 | $model = $this->model; 238 | 239 | $result = $model->withoutEvents(function () { 240 | return $this->delete(); 241 | }); 242 | 243 | // Still flush cache even though events aren't fired 244 | if ($result && method_exists($model, 'flushModelCache')) { 245 | $model->flushModelCache(); 246 | } elseif ($result) { 247 | $this->flushQueryCache(); 248 | } 249 | 250 | return $result; 251 | } 252 | 253 | /** 254 | * Touch all of the related models for the relationship. 255 | * 256 | * @param null $column 257 | * @return void 258 | */ 259 | public function touch($column = null) 260 | { 261 | parent::touch($column); 262 | 263 | // Flush cache 264 | if (method_exists($this->model, 'flushModelCache')) { 265 | $this->model->flushModelCache(); 266 | } else { 267 | $this->flushQueryCache(); 268 | } 269 | } 270 | 271 | /** 272 | * Execute the query and get the first result from the cache. 273 | * 274 | * @param array $columns 275 | * @return \Illuminate\Database\Eloquent\Model|\stdClass|static|null 276 | */ 277 | public function firstFromCache($columns = ['*']) 278 | { 279 | // Check if caching is globally enabled 280 | if (config('model-cache.enabled', true) === false) { 281 | $results = $this->take(1)->getWithoutCache($columns); 282 | return count($results) > 0 ? $results->first() : null; 283 | } 284 | 285 | $results = $this->take(1)->getFromCache($columns); 286 | 287 | return count($results) > 0 ? $results->first() : null; 288 | } 289 | 290 | /** 291 | * Execute the query and get the results from the cache. 292 | * 293 | * @param array $columns 294 | * @return \Illuminate\Database\Eloquent\Collection 295 | */ 296 | public function getFromCache($columns = ['*']) 297 | { 298 | // Check if caching is globally enabled 299 | if (config('model-cache.enabled', true) === false) { 300 | return $this->getWithoutCache($columns); 301 | } 302 | 303 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 304 | $cacheKey = $this->getCacheKey($columns); 305 | $cacheTags = $this->getCacheTags(); 306 | $cache = $this->getCacheDriver(); 307 | 308 | // Check if the cache driver supports tags 309 | $supportsTags = $this->supportsTags($cache); 310 | 311 | if ($cacheTags && $supportsTags) { 312 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, function () use ($columns) { 313 | return $this->getWithoutCache($columns); 314 | }); 315 | } 316 | 317 | // Fallback for drivers that don't support tagging 318 | return $cache->remember($cacheKey, $minutes * 60, function () use ($columns) { 319 | return $this->getWithoutCache($columns); 320 | }); 321 | } 322 | 323 | /** 324 | * Check if the cache driver supports tags. 325 | * 326 | * @param \Illuminate\Contracts\Cache\Repository $cache 327 | * @return bool 328 | */ 329 | protected function supportsTags($cache) 330 | { 331 | try { 332 | return method_exists($cache, 'tags') && $cache->supportsTags(); 333 | } catch (\Exception $e) { 334 | return false; 335 | } 336 | } 337 | 338 | /** 339 | * Set the cache duration. 340 | * 341 | * @param int $minutes 342 | * @return $this 343 | */ 344 | public function remember($minutes) 345 | { 346 | // Don't set cache minutes if caching is globally disabled 347 | if (config('model-cache.enabled', true) === false) { 348 | return $this->withoutCache(); 349 | } 350 | 351 | $this->cacheMinutes = $minutes; 352 | 353 | return $this; 354 | } 355 | 356 | /** 357 | * Disable caching for this query. 358 | * 359 | * @return $this 360 | */ 361 | public function withoutCache() 362 | { 363 | $this->cacheMinutes = 0; 364 | 365 | return $this; 366 | } 367 | 368 | /** 369 | * Get a unique cache key for the complete query. 370 | * 371 | * @param array $columns 372 | * @return string 373 | */ 374 | public function getCacheKey($columns = ['*']) 375 | { 376 | // This is the prefix defined in our package config 377 | $configPrefix = $this->cachePrefix ?? config('model-cache.cache_key_prefix', 'model_cache_'); 378 | 379 | 380 | // Create unique components for our key 381 | $keyComponents = [ 382 | $configPrefix, 383 | $this->model->getTable(), 384 | $this->toSql(), 385 | serialize($this->getBindings()), 386 | serialize($columns), 387 | app()->getLocale(), // Add locale for multilingual sites 388 | ]; 389 | 390 | // Include eager loaded relationships in the cache key 391 | if (count($this->eagerLoad) > 0) { 392 | $keyComponents[] = 'with:' . serialize(array_keys($this->eagerLoad)); 393 | } 394 | 395 | // Create a hash from all components 396 | $uniqueKey = md5(implode('|', $keyComponents)); 397 | 398 | // Add debug logging if enabled 399 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 400 | logger()->debug("Generated cache key: {$uniqueKey} for query: {$this->toSql()} with bindings: " . json_encode($this->getBindings()) . " and relations: " . json_encode(array_keys($this->eagerLoad))); 401 | } 402 | 403 | // Return only the unique hash - Laravel will handle adding its own prefix 404 | return $uniqueKey; 405 | } 406 | 407 | /** 408 | * Flush the cache for this specific query. 409 | * 410 | * This method is called in two primary situations: 411 | * 1. Explicitly by the user: Model::where('condition', $value)->flushCache(); 412 | * 2. Automatically after mass operations like update(), delete(), etc. 413 | * 414 | * The method attempts to clear the cache in three ways, in order of specificity: 415 | * 1. First, it tries to remove the specific cache key for this query 416 | * 2. If the cache driver supports tags, it tries to flush by model-specific tags 417 | * 3. As a fallback, it calls the model's flushModelCache() method 418 | * 419 | * @param array $columns 420 | * @return bool 421 | */ 422 | public function flushQueryCache($columns = ['*']) 423 | { 424 | try { 425 | // Get the specific key for this query 426 | $cacheKey = $this->getCacheKey($columns); 427 | $cache = $this->getCacheDriver(); 428 | 429 | // Log the operation if logger is available 430 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 431 | logger()->info("Flushing specific query cache: " . $cacheKey); 432 | logger()->debug("SQL: " . $this->toSql()); 433 | logger()->debug("Bindings: " . json_encode($this->getBindings())); 434 | } 435 | 436 | $success = false; 437 | 438 | // First try to forget this specific key 439 | $result = $cache->forget($cacheKey); 440 | if ($result) { 441 | $success = true; 442 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 443 | logger()->debug("Successfully removed specific cache key: " . $cacheKey); 444 | } 445 | } 446 | 447 | // Also try with tags if supported 448 | $cacheTags = $this->getCacheTags(); 449 | if ($cacheTags && $this->supportsTags($cache)) { 450 | try { 451 | // First try model-specific tags 452 | $cache->tags($cacheTags)->flush(); 453 | 454 | // Then try query-specific tags to be even more precise 455 | $queryTags = $cacheTags; 456 | $queryTags[] = md5($this->toSql() . serialize($this->getBindings())); 457 | $cache->tags($queryTags)->flush(); 458 | 459 | $success = true; 460 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 461 | logger()->debug("Successfully flushed cache using tags for model: " . get_class($this->model)); 462 | } 463 | } catch (\Exception $e) { 464 | // If this fails, we already tried the direct key removal above 465 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 466 | logger()->debug("Could not flush by query tags: " . $e->getMessage()); 467 | } 468 | } 469 | } 470 | 471 | // If both specific key and tags failed, try to flush related model cache 472 | if (!$success) { 473 | if (method_exists($this->model, 'flushModelCache')) { 474 | $this->model->flushModelCache(); 475 | $success = true; 476 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 477 | logger()->info("Flushed entire model cache for: " . get_class($this->model)); 478 | } 479 | } 480 | } 481 | 482 | return $success || $result; 483 | } catch (\Exception $e) { 484 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 485 | logger()->error("Error flushing query cache: " . $e->getMessage()); 486 | } 487 | return false; 488 | } 489 | } 490 | 491 | /** 492 | * Alias for flushQueryCache for backward compatibility with existing code. 493 | * 494 | * @param array $columns 495 | * @return bool 496 | */ 497 | public function flushCache($columns = ['*']) 498 | { 499 | return $this->flushQueryCache($columns); 500 | } 501 | 502 | /** 503 | * Get the cache tags for the query. 504 | * 505 | * @return array 506 | */ 507 | protected function getCacheTags() 508 | { 509 | return [ 510 | 'model_cache', 511 | get_class($this->model), 512 | $this->model->getTable() 513 | ]; 514 | } 515 | 516 | /** 517 | * Get the models without cache. 518 | * 519 | * @param array $columns 520 | * @return Collection 521 | */ 522 | protected function getWithoutCache($columns = ['*']) 523 | { 524 | return parent::get($columns); 525 | } 526 | 527 | /** 528 | * Override the get method to automatically use cache. 529 | * 530 | * @param array $columns 531 | * @return \Illuminate\Database\Eloquent\Collection 532 | */ 533 | public function get($columns = ['*']) 534 | { 535 | // Only use cache if cacheMinutes is not set to 0 536 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 537 | return $this->getWithoutCache($columns); 538 | } 539 | 540 | return $this->getFromCache($columns); 541 | } 542 | 543 | /** 544 | * Get the cache driver to use. 545 | * 546 | * @return Repository 547 | */ 548 | protected function getCacheDriver() 549 | { 550 | try { 551 | $cacheStore = config('model-cache.cache_store'); 552 | 553 | if ($cacheStore) { 554 | return Cache::store($cacheStore); 555 | } 556 | 557 | return Cache::store(); 558 | } catch (\Exception $e) { 559 | // If there's an issue with the configured cache driver, 560 | // fall back to the default driver 561 | return Cache::store(config('cache.default')); 562 | } 563 | } 564 | 565 | /** 566 | * Paginate the given query with caching support. 567 | * 568 | * @param int|null $perPage 569 | * @param array $columns 570 | * @param string $pageName 571 | * @param int|null $page 572 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 573 | */ 574 | public function paginateFromCache($perPage = null, $columns = ['*'], $pageName = 'page', $page = null) 575 | { 576 | $page = $page ?: \Illuminate\Pagination\Paginator::resolveCurrentPage($pageName); 577 | 578 | $perPage = $perPage ?: $this->model->getPerPage(); 579 | 580 | $cacheKey = $this->getCacheKey([ 581 | 'paginate', 582 | $perPage, 583 | $pageName, 584 | $page, 585 | serialize($columns) 586 | ]); 587 | 588 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 589 | $cacheTags = $this->getCacheTags(); 590 | $cache = $this->getCacheDriver(); 591 | 592 | $callback = function () use ($perPage, $columns, $pageName, $page) { 593 | return parent::paginate($perPage, $columns, $pageName, $page); 594 | }; 595 | 596 | try { 597 | // Check if the cache driver supports tags 598 | if ($cacheTags && $this->supportsTags($cache)) { 599 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 600 | } 601 | } catch (\BadMethodCallException $e) { 602 | // If tags are not supported, we'll fall through to the default behavior 603 | } 604 | 605 | return $cache->remember($cacheKey, $minutes * 60, $callback); 606 | } 607 | 608 | /** 609 | * Retrieve the "count" result of the query from cache. 610 | * 611 | * @param string $columns 612 | * @return int 613 | */ 614 | public function countFromCache($columns = '*') 615 | { 616 | $cacheKey = $this->getCacheKey([ 617 | 'count', 618 | is_array($columns) ? implode(',', $columns) : $columns 619 | ]); 620 | 621 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 622 | $cacheTags = $this->getCacheTags(); 623 | $cache = $this->getCacheDriver(); 624 | 625 | $callback = function () use ($columns) { 626 | return parent::count($columns); 627 | }; 628 | 629 | if ($cacheTags && $this->supportsTags($cache)) { 630 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 631 | } 632 | 633 | return $cache->remember($cacheKey, $minutes * 60, $callback); 634 | } 635 | 636 | /** 637 | * Retrieve the sum of the values of a given column from cache. 638 | * 639 | * @param string $column 640 | * @return mixed 641 | */ 642 | public function sumFromCache($column) 643 | { 644 | $cacheKey = $this->getCacheKey([ 645 | 'sum', 646 | $column 647 | ]); 648 | 649 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 650 | $cacheTags = $this->getCacheTags(); 651 | $cache = $this->getCacheDriver(); 652 | 653 | $callback = function () use ($column) { 654 | return parent::sum($column); 655 | }; 656 | 657 | if ($cacheTags && $this->supportsTags($cache)) { 658 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 659 | } 660 | 661 | return $cache->remember($cacheKey, $minutes * 60, $callback); 662 | } 663 | 664 | /** 665 | * Retrieve the maximum value of a given column from cache. 666 | * 667 | * @param string $column 668 | * @return mixed 669 | */ 670 | public function maxFromCache($column) 671 | { 672 | $cacheKey = $this->getCacheKey([ 673 | 'max', 674 | $column 675 | ]); 676 | 677 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 678 | $cacheTags = $this->getCacheTags(); 679 | $cache = $this->getCacheDriver(); 680 | 681 | $callback = function () use ($column) { 682 | return parent::max($column); 683 | }; 684 | 685 | if ($cacheTags && $this->supportsTags($cache)) { 686 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 687 | } 688 | 689 | return $cache->remember($cacheKey, $minutes * 60, $callback); 690 | } 691 | 692 | /** 693 | * Retrieve the minimum value of a given column from cache. 694 | * 695 | * @param string $column 696 | * @return mixed 697 | */ 698 | public function minFromCache($column) 699 | { 700 | $cacheKey = $this->getCacheKey([ 701 | 'min', 702 | $column 703 | ]); 704 | 705 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 706 | $cacheTags = $this->getCacheTags(); 707 | $cache = $this->getCacheDriver(); 708 | 709 | $callback = function () use ($column) { 710 | return parent::min($column); 711 | }; 712 | 713 | if ($cacheTags && $this->supportsTags($cache)) { 714 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 715 | } 716 | 717 | return $cache->remember($cacheKey, $minutes * 60, $callback); 718 | } 719 | 720 | /** 721 | * Retrieve the average of the values of a given column from cache. 722 | * 723 | * @param string $column 724 | * @return mixed 725 | */ 726 | public function avgFromCache($column) 727 | { 728 | $cacheKey = $this->getCacheKey([ 729 | 'avg', 730 | $column 731 | ]); 732 | 733 | $minutes = $this->cacheMinutes ?: config('model-cache.cache_duration', 60); 734 | $cacheTags = $this->getCacheTags(); 735 | $cache = $this->getCacheDriver(); 736 | 737 | $callback = function () use ($column) { 738 | return parent::avg($column); 739 | }; 740 | 741 | if ($cacheTags && $this->supportsTags($cache)) { 742 | return $cache->tags($cacheTags)->remember($cacheKey, $minutes * 60, $callback); 743 | } 744 | 745 | return $cache->remember($cacheKey, $minutes * 60, $callback); 746 | } 747 | 748 | /** 749 | * Override the count method to automatically use cache. 750 | * 751 | * @param string $columns 752 | * @return int 753 | */ 754 | public function count($columns = '*') 755 | { 756 | // Only use cache if cacheMinutes is not set to 0 757 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 758 | return parent::count($columns); 759 | } 760 | 761 | return $this->countFromCache($columns); 762 | } 763 | 764 | /** 765 | * Override the sum method to automatically use cache. 766 | * 767 | * @param string $column 768 | * @return mixed 769 | */ 770 | public function sum($column) 771 | { 772 | // Only use cache if cacheMinutes is not set to 0 773 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 774 | return parent::sum($column); 775 | } 776 | 777 | return $this->sumFromCache($column); 778 | } 779 | 780 | /** 781 | * Override the max method to automatically use cache. 782 | * 783 | * @param string $column 784 | * @return mixed 785 | */ 786 | public function max($column) 787 | { 788 | // Only use cache if cacheMinutes is not set to 0 789 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 790 | return parent::max($column); 791 | } 792 | 793 | return $this->maxFromCache($column); 794 | } 795 | 796 | /** 797 | * Override the min method to automatically use cache. 798 | * 799 | * @param string $column 800 | * @return mixed 801 | */ 802 | public function min($column) 803 | { 804 | // Only use cache if cacheMinutes is not set to 0 805 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 806 | return parent::min($column); 807 | } 808 | 809 | return $this->minFromCache($column); 810 | } 811 | 812 | /** 813 | * Override the avg method to automatically use cache. 814 | * 815 | * @param string $column 816 | * @return mixed 817 | */ 818 | public function avg($column) 819 | { 820 | // Only use cache if cacheMinutes is not set to 0 821 | if (isset($this->cacheMinutes) && $this->cacheMinutes === 0) { 822 | return parent::avg($column); 823 | } 824 | 825 | return $this->avgFromCache($column); 826 | } 827 | 828 | /** 829 | * Alias for the "avg" method. 830 | * 831 | * @param string $column 832 | * @return mixed 833 | */ 834 | public function average($column) 835 | { 836 | return $this->avg($column); 837 | } 838 | 839 | /** 840 | * Update records in the database and flush cache. 841 | * 842 | * @param array $values 843 | * @return int 844 | */ 845 | public function update(array $values) 846 | { 847 | // Execute the update operation 848 | $result = parent::update($values); 849 | 850 | // Flush the cache for this model 851 | if ($result) { 852 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 853 | logger()->info("Flushing cache after mass update for model: " . get_class($this->model)); 854 | } 855 | 856 | // Try to flush the model cache 857 | if (method_exists($this->model, 'flushModelCache')) { 858 | $this->model->flushModelCache(); 859 | } else { 860 | // Fallback to flushing the query cache 861 | $this->flushQueryCache(); 862 | } 863 | } 864 | 865 | return $result; 866 | } 867 | 868 | /** 869 | * Delete records from the database and flush cache. 870 | * 871 | * @return mixed 872 | */ 873 | public function delete() 874 | { 875 | // Execute the delete operation 876 | $result = parent::delete(); 877 | 878 | // Flush the cache for this model if any records were deleted 879 | if ($result) { 880 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 881 | logger()->info("Flushing cache after mass delete for model: " . get_class($this->model)); 882 | } 883 | 884 | // Try to flush the model cache 885 | if (method_exists($this->model, 'flushModelCache')) { 886 | $this->model->flushModelCache(); 887 | } else { 888 | // Fallback to flushing the query cache 889 | $this->flushQueryCache(); 890 | } 891 | } 892 | 893 | return $result; 894 | } 895 | 896 | /** 897 | * Increment a column's value by a given amount and flush cache. 898 | * 899 | * @param string $column 900 | * @param float|int $amount 901 | * @param array $extra 902 | * @return int 903 | */ 904 | public function increment($column, $amount = 1, array $extra = []) 905 | { 906 | // Execute the increment operation 907 | $result = parent::increment($column, $amount, $extra); 908 | 909 | // Flush the cache for this model 910 | if ($result) { 911 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 912 | logger()->info("Flushing cache after increment operation for model: " . get_class($this->model)); 913 | } 914 | 915 | // Try to flush the model cache 916 | if (method_exists($this->model, 'flushModelCache')) { 917 | $this->model->flushModelCache(); 918 | } else { 919 | // Fallback to flushing the query cache 920 | $this->flushQueryCache(); 921 | } 922 | } 923 | 924 | return $result; 925 | } 926 | 927 | /** 928 | * Decrement a column's value by a given amount and flush cache. 929 | * 930 | * @param string $column 931 | * @param float|int $amount 932 | * @param array $extra 933 | * @return int 934 | */ 935 | public function decrement($column, $amount = 1, array $extra = []) 936 | { 937 | // Execute the decrement operation 938 | $result = parent::decrement($column, $amount, $extra); 939 | 940 | // Flush the cache for this model 941 | if ($result) { 942 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 943 | logger()->info("Flushing cache after decrement operation for model: " . get_class($this->model)); 944 | } 945 | 946 | // Try to flush the model cache 947 | if (method_exists($this->model, 'flushModelCache')) { 948 | $this->model->flushModelCache(); 949 | } else { 950 | // Fallback to flushing the query cache 951 | $this->flushQueryCache(); 952 | } 953 | } 954 | 955 | return $result; 956 | } 957 | 958 | /** 959 | * Insert new records into the database and flush cache. 960 | * 961 | * @param array $values 962 | * @return bool 963 | */ 964 | public function insert(array $values) 965 | { 966 | // Execute the insert operation 967 | $result = parent::insert($values); 968 | 969 | // Flush the cache for this model if insert was successful 970 | if ($result) { 971 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 972 | logger()->info("Flushing cache after insert operation for model: " . get_class($this->model)); 973 | } 974 | 975 | // Try to flush the model cache 976 | if (method_exists($this->model, 'flushModelCache')) { 977 | $this->model->flushModelCache(); 978 | } else { 979 | // Fallback to flushing the query cache 980 | $this->flushQueryCache(); 981 | } 982 | } 983 | 984 | return $result; 985 | } 986 | 987 | /** 988 | * Insert new records into the database while ignoring errors and flush cache. 989 | * 990 | * @param array $values 991 | * @return int 992 | */ 993 | public function insertOrIgnore(array $values) 994 | { 995 | // Execute the insertOrIgnore operation 996 | $result = parent::insertOrIgnore($values); 997 | 998 | // Flush the cache for this model if any records were inserted 999 | if ($result > 0) { 1000 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1001 | logger()->info("Flushing cache after insertOrIgnore operation for model: " . get_class($this->model)); 1002 | } 1003 | 1004 | // Try to flush the model cache 1005 | if (method_exists($this->model, 'flushModelCache')) { 1006 | $this->model->flushModelCache(); 1007 | } else { 1008 | // Fallback to flushing the query cache 1009 | $this->flushQueryCache(); 1010 | } 1011 | } 1012 | 1013 | return $result; 1014 | } 1015 | 1016 | /** 1017 | * Insert a new record and get the value of the primary key and flush cache. 1018 | * 1019 | * @param array $values 1020 | * @param string|null $sequence 1021 | * @return int 1022 | */ 1023 | public function insertGetId(array $values, $sequence = null) 1024 | { 1025 | // Execute the insertGetId operation 1026 | $result = parent::insertGetId($values, $sequence); 1027 | 1028 | // Flush the cache for this model 1029 | if ($result) { 1030 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1031 | logger()->info("Flushing cache after insertGetId operation for model: " . get_class($this->model)); 1032 | } 1033 | 1034 | // Try to flush the model cache 1035 | if (method_exists($this->model, 'flushModelCache')) { 1036 | $this->model->flushModelCache(); 1037 | } else { 1038 | // Fallback to flushing the query cache 1039 | $this->flushQueryCache(); 1040 | } 1041 | } 1042 | 1043 | return $result; 1044 | } 1045 | 1046 | /** 1047 | * Insert or update a record matching the attributes, and fill it with values. 1048 | * 1049 | * @param array $attributes 1050 | * @param array $values 1051 | * @return bool 1052 | */ 1053 | public function updateOrInsert(array $attributes, $values = []) 1054 | { 1055 | // Execute the updateOrInsert operation 1056 | $result = parent::updateOrInsert($attributes, $values); 1057 | 1058 | // Flush the cache for this model if operation was successful 1059 | if ($result) { 1060 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1061 | logger()->info("Flushing cache after updateOrInsert operation for model: " . get_class($this->model)); 1062 | } 1063 | 1064 | // Try to flush the model cache 1065 | if (method_exists($this->model, 'flushModelCache')) { 1066 | $this->model->flushModelCache(); 1067 | } else { 1068 | // Fallback to flushing the query cache 1069 | $this->flushQueryCache(); 1070 | } 1071 | } 1072 | 1073 | return $result; 1074 | } 1075 | 1076 | /** 1077 | * Insert new records or update the existing ones and flush cache. 1078 | * 1079 | * @param array $values 1080 | * @param array|string $uniqueBy 1081 | * @param array|null $update 1082 | * @return int 1083 | */ 1084 | public function upsert(array $values, $uniqueBy, $update = null) 1085 | { 1086 | // Check if upsert method exists in the parent (Laravel 8+) 1087 | if (!method_exists(get_parent_class($this), 'upsert')) { 1088 | throw new \BadMethodCallException('Method upsert() is not supported by the database driver.'); 1089 | } 1090 | 1091 | // Execute the upsert operation 1092 | $result = parent::upsert($values, $uniqueBy, $update); 1093 | 1094 | // Flush the cache for this model if any records were affected 1095 | if ($result > 0) { 1096 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1097 | logger()->info("Flushing cache after upsert operation for model: " . get_class($this->model)); 1098 | } 1099 | 1100 | // Try to flush the model cache 1101 | if (method_exists($this->model, 'flushModelCache')) { 1102 | $this->model->flushModelCache(); 1103 | } else { 1104 | // Fallback to flushing the query cache 1105 | $this->flushQueryCache(); 1106 | } 1107 | } 1108 | 1109 | return $result; 1110 | } 1111 | 1112 | /** 1113 | * Truncate the table and flush cache. 1114 | * 1115 | * @return void 1116 | */ 1117 | public function truncate() 1118 | { 1119 | // Execute the truncate operation 1120 | parent::truncate(); 1121 | 1122 | // Always flush the cache after truncate 1123 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1124 | logger()->info("Flushing cache after truncate operation for model: " . get_class($this->model)); 1125 | } 1126 | 1127 | // Try to flush the model cache 1128 | if (method_exists($this->model, 'flushModelCache')) { 1129 | $this->model->flushModelCache(); 1130 | } else { 1131 | // Fallback to flushing the query cache 1132 | $this->flushQueryCache(); 1133 | } 1134 | } 1135 | 1136 | /** 1137 | * Force a hard delete on a soft deleted model and flush cache. 1138 | * This method overrides the forceDelete method present in the SoftDeletes trait. 1139 | * 1140 | * @return mixed 1141 | */ 1142 | public function forceDelete() 1143 | { 1144 | // Check if the model uses SoftDeletes 1145 | if (!method_exists($this->model, 'runSoftDelete')) { 1146 | return $this->delete(); 1147 | } 1148 | 1149 | // Execute the force delete operation 1150 | $result = parent::forceDelete(); 1151 | 1152 | // Flush the cache for this model 1153 | if ($result) { 1154 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1155 | logger()->info("Flushing cache after force delete for model: " . get_class($this->model)); 1156 | } 1157 | 1158 | // Try to flush the model cache 1159 | if (method_exists($this->model, 'flushModelCache')) { 1160 | $this->model->flushModelCache(); 1161 | } else { 1162 | // Fallback to flushing the query cache 1163 | $this->flushQueryCache(); 1164 | } 1165 | } 1166 | 1167 | return $result; 1168 | } 1169 | 1170 | /** 1171 | * Restore soft deleted models and flush cache. 1172 | * This method overrides the restore method present in the SoftDeletes trait. 1173 | * 1174 | * @return mixed 1175 | */ 1176 | public function restore() 1177 | { 1178 | // Check if the model uses SoftDeletes 1179 | if (!method_exists($this->model, 'runSoftDelete')) { 1180 | return 0; 1181 | } 1182 | 1183 | // Execute the restore operation 1184 | $result = parent::restore(); 1185 | 1186 | // Flush the cache for this model 1187 | if ($result) { 1188 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 1189 | logger()->info("Flushing cache after restore for model: " . get_class($this->model)); 1190 | } 1191 | 1192 | // Try to flush the model cache 1193 | if (method_exists($this->model, 'flushModelCache')) { 1194 | $this->model->flushModelCache(); 1195 | } else { 1196 | // Fallback to flushing the query cache 1197 | $this->flushQueryCache(); 1198 | } 1199 | } 1200 | 1201 | return $result; 1202 | } 1203 | } 1204 | -------------------------------------------------------------------------------- /src/CachingBelongsToMany.php: -------------------------------------------------------------------------------- 1 | cacheableParent = $cacheableParent ?: $parent; 42 | } 43 | 44 | /** 45 | * Attach a model to the parent. 46 | * 47 | * @param mixed $id 48 | * @param array $attributes 49 | * @param bool $touch 50 | * @return void 51 | */ 52 | public function attach($id, array $attributes = [], $touch = true) 53 | { 54 | // Call parent method to perform the actual attach 55 | parent::attach($id, $attributes, $touch); 56 | 57 | // Flush cache after operation 58 | $this->flushCache('attach'); 59 | } 60 | 61 | /** 62 | * Detach models from the relationship. 63 | * 64 | * @param mixed $ids 65 | * @param bool $touch 66 | * @return int 67 | */ 68 | public function detach($ids = null, $touch = true) 69 | { 70 | // Call parent method to perform the actual detach 71 | $result = parent::detach($ids, $touch); 72 | 73 | // Flush cache after operation 74 | $this->flushCache('detach'); 75 | 76 | return $result; 77 | } 78 | 79 | /** 80 | * Sync the intermediate tables with a list of IDs or collection of models. 81 | * 82 | * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids 83 | * @param bool $detaching 84 | * @return array 85 | */ 86 | public function sync($ids, $detaching = true) 87 | { 88 | // Call parent method to perform the actual sync 89 | $result = parent::sync($ids, $detaching); 90 | 91 | // Flush cache after operation 92 | $this->flushCache('sync'); 93 | 94 | return $result; 95 | } 96 | 97 | /** 98 | * Update an existing pivot record on the table. 99 | * 100 | * @param mixed $id 101 | * @param array $attributes 102 | * @param bool $touch 103 | * @return int 104 | */ 105 | public function updateExistingPivot($id, array $attributes, $touch = true) 106 | { 107 | // Call parent method to perform the actual update 108 | $result = parent::updateExistingPivot($id, $attributes, $touch); 109 | 110 | // Flush cache after operation 111 | $this->flushCache('updateExistingPivot'); 112 | 113 | return $result; 114 | } 115 | 116 | /** 117 | * Sync the intermediate tables with a list of IDs without detaching. 118 | * 119 | * @param \Illuminate\Support\Collection|\Illuminate\Database\Eloquent\Model|array $ids 120 | * @return array 121 | */ 122 | public function syncWithoutDetaching($ids) 123 | { 124 | // Call parent method to perform the actual sync 125 | $result = parent::syncWithoutDetaching($ids); 126 | 127 | // Flush cache after operation 128 | $this->flushCache('syncWithoutDetaching'); 129 | 130 | return $result; 131 | } 132 | 133 | /** 134 | * Flush the model cache after a relationship operation. 135 | * 136 | * @param string $operation 137 | * @return void 138 | */ 139 | protected function flushCache($operation) 140 | { 141 | if (method_exists($this->cacheableParent, 'flushModelCache')) { 142 | $this->cacheableParent->flushModelCache(); 143 | } else { 144 | if (method_exists($this->cacheableParent, 'flushCache')) { 145 | $this->cacheableParent->flushCache(); 146 | } else { 147 | throw new \Exception('The parent model must have a flushCache() or flushModelCache() method defined. Make sure your model uses the HasCachedQueries trait. The ModelRelationships trait should be used in conjunction with the HasCachedQueries trait. See the documentation for more information.'); 148 | } 149 | } 150 | 151 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 152 | logger()->info("Cache flushed after {$operation} operation for model: " . get_class($this->cacheableParent)); 153 | } 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Console/Commands/ClearModelCacheCommand.php: -------------------------------------------------------------------------------- 1 | argument('model'); 33 | 34 | if ($modelClass) { 35 | $this->clearModelCache($modelClass); 36 | } else { 37 | $this->clearAllModelCache(); 38 | } 39 | 40 | return CommandAlias::SUCCESS; 41 | } 42 | 43 | /** 44 | * Clear cache for a specific model. 45 | * 46 | * @param string $modelClass 47 | * @return void 48 | */ 49 | protected function clearModelCache(string $modelClass) 50 | { 51 | if (!class_exists($modelClass)) { 52 | $this->error("Model class {$modelClass} does not exist!"); 53 | return; 54 | } 55 | 56 | $this->info("Attempting to clear cache for model: {$modelClass}"); 57 | 58 | // Check if the model uses our trait 59 | if (!$this->usesHasCachedQueriesTrait($modelClass)) { 60 | $this->warn("Warning: The model {$modelClass} doesn't use HasCachedQueries trait. Cache functionality might be limited."); 61 | } 62 | 63 | // Show the current cache configuration 64 | $this->info("Current cache driver: " . config('cache.default')); 65 | $this->info("Model cache store: " . config('model-cache.cache_store', 'default')); 66 | 67 | try { 68 | // First check if the model has static flush methods 69 | if (method_exists($modelClass, 'flushModelCache') && 70 | is_callable([$modelClass, 'flushModelCache']) && 71 | (new \ReflectionMethod($modelClass, 'flushModelCache'))->isStatic()) { 72 | // For backward compatibility - check if static flushModelCache exists 73 | $this->info("Found static method: flushModelCache"); 74 | $result = $modelClass::flushModelCache(); 75 | if ($result) { 76 | $this->info("Cache cleared successfully for model: {$modelClass} using static method"); 77 | } else { 78 | $this->warn("Static method returned false - cache may not have been cleared completely"); 79 | // Force a full cache clear as a backup 80 | $this->performFullCacheFlush(); 81 | } 82 | return; 83 | } 84 | 85 | $this->info("No static methods found. Trying with instance methods..."); 86 | 87 | // If no static methods, try instance methods 88 | $model = new $modelClass(); 89 | $tableName = $model->getTable(); 90 | $this->info("Model table: {$tableName}"); 91 | 92 | if (method_exists($model, 'flushCache')) { 93 | $this->info("Found instance method: flushCache"); 94 | $result = $model->flushCache(); 95 | if ($result) { 96 | $this->info("Cache cleared successfully for model: {$modelClass}"); 97 | } else { 98 | $this->warn("Instance method returned false - cache may not have been cleared completely"); 99 | // Force a full cache clear as a backup 100 | $this->performFullCacheFlush(); 101 | } 102 | } elseif (method_exists($model, 'flushModelCache')) { 103 | // For backward compatibility 104 | $this->info("Found instance method: flushModelCache"); 105 | $result = $model::flushModelCache(); 106 | if ($result) { 107 | $this->info("Cache cleared successfully for model: {$modelClass}"); 108 | } else { 109 | $this->warn("Instance method returned false - cache may not have been cleared completely"); 110 | // Force a full cache clear as a backup 111 | $this->performFullCacheFlush(); 112 | } 113 | } else { 114 | $this->warn("No cache flush methods found on the model. Using manual clearing..."); 115 | $this->clearModelCacheManually($modelClass, $tableName); 116 | } 117 | } catch (\Exception $e) { 118 | $this->error("Error clearing cache for {$modelClass}: " . $e->getMessage()); 119 | $this->error("Stack trace: " . $e->getTraceAsString()); 120 | 121 | // Ask if user wants to try full cache flush as a last resort 122 | if ($this->confirm('Would you like to clear the entire application cache?', true)) { 123 | $this->performFullCacheFlush(); 124 | } 125 | } 126 | } 127 | 128 | /** 129 | * Check if a model uses the HasCachedQueries trait. 130 | * 131 | * @param string $class 132 | * @return bool 133 | */ 134 | protected function usesHasCachedQueriesTrait($class) 135 | { 136 | $traits = class_uses_recursive($class); 137 | return isset($traits['YMigVal\LaravelModelCache\HasCachedQueries']); 138 | } 139 | 140 | /** 141 | * Perform a full cache flush as a last resort. 142 | * 143 | * @return void 144 | */ 145 | protected function performFullCacheFlush() 146 | { 147 | $this->info("Performing full cache flush as a fallback..."); 148 | 149 | try { 150 | // Get the cache driver 151 | $cacheStore = config('model-cache.cache_store'); 152 | $cache = $cacheStore ? \Illuminate\Support\Facades\Cache::store($cacheStore) : \Illuminate\Support\Facades\Cache::store(); 153 | 154 | // Flush everything 155 | $cache->flush(); 156 | $this->info("Full application cache has been cleared successfully"); 157 | } catch (\Exception $e) { 158 | $this->error("Error performing full cache flush: " . $e->getMessage()); 159 | } 160 | } 161 | 162 | /** 163 | * Clear cache manually when flushModelCache is not available. 164 | * 165 | * @param string $modelClass 166 | * @param string $tableName 167 | * @return void 168 | */ 169 | protected function clearModelCacheManually(string $modelClass, string $tableName) 170 | { 171 | try { 172 | // Try to get the configured cache store 173 | $cacheStore = config('model-cache.cache_store'); 174 | $cache = $cacheStore ? Cache::store($cacheStore) : Cache::store(); 175 | 176 | $tags = ['model_cache', $modelClass, $tableName]; 177 | 178 | // First try to use tags if supported 179 | if ($this->supportsTags($cache)) { 180 | try { 181 | $cache->tags($tags)->flush(); 182 | $this->info("Cache cleared for model: {$modelClass} using tags"); 183 | return; 184 | } catch (\Exception $e) { 185 | $this->warn("Error using cache tags: " . $e->getMessage()); 186 | } 187 | } 188 | 189 | // If we reach here, tags are not supported or failed 190 | // For simplicity, just confirm and clear all cache 191 | if ($this->confirm("Your cache driver doesn't support tags or there was an error. Would you like to clear ALL application cache?", false)) { 192 | $cache->flush(); 193 | $this->info("All cache cleared successfully"); 194 | } else { 195 | $this->info("Cache clearing cancelled"); 196 | } 197 | 198 | } catch (\Exception $e) { 199 | $this->error("Error clearing cache: " . $e->getMessage()); 200 | } 201 | } 202 | 203 | /** 204 | * Clear cache for all models. 205 | * 206 | * @return void 207 | */ 208 | protected function clearAllModelCache() 209 | { 210 | try { 211 | // Try to get the configured cache store 212 | $cacheStore = config('model-cache.cache_store'); 213 | $cache = $cacheStore ? Cache::store($cacheStore) : Cache::store(); 214 | 215 | // First try to use tags if supported 216 | if ($this->supportsTags($cache)) { 217 | try { 218 | $cache->tags('model_cache')->flush(); 219 | $this->info("Cache cleared for all models using tags"); 220 | return; 221 | } catch (\Exception $e) { 222 | $this->warn("Error using cache tags: " . $e->getMessage()); 223 | } 224 | } 225 | 226 | // If we reach here, tags are not supported or failed 227 | // Ask for confirmation before clearing all cache 228 | if ($this->confirm('Your cache driver doesn\'t support tags. This will clear ALL application cache. Continue?', false)) { 229 | $cache->flush(); 230 | $this->info("All cache cleared successfully"); 231 | } else { 232 | $this->info("Cache clearing cancelled"); 233 | } 234 | 235 | } catch (\Exception $e) { 236 | $this->error("Error clearing cache: " . $e->getMessage()); 237 | } 238 | } 239 | 240 | /** 241 | * Check if the cache repository supports tagging. 242 | * 243 | * @param \Illuminate\Contracts\Cache\Repository $cache 244 | * @return bool 245 | */ 246 | protected function supportsTags($cache) 247 | { 248 | try { 249 | return method_exists($cache, 'tags') && $cache->supportsTags(); 250 | } catch (\Exception $e) { 251 | return false; 252 | } 253 | } 254 | 255 | /** 256 | * Performs a full cache clear when tags aren't supported. 257 | * 258 | * @param \Illuminate\Contracts\Cache\Repository $cache 259 | * @return void 260 | */ 261 | protected function performFullCacheClear($cache) 262 | { 263 | $this->warn("Your cache driver doesn't support tags. Using full cache clear..."); 264 | 265 | // Optionally, you can ask for confirmation before clearing all cache 266 | if ($this->confirm('This will clear ALL application cache, not just model cache. Continue?', true)) { 267 | $cache->flush(); 268 | $this->info("All cache cleared (cannot target specific model without tags support)"); 269 | } else { 270 | $this->info("Cache clearing cancelled"); 271 | } 272 | } 273 | } 274 | -------------------------------------------------------------------------------- /src/HasCachedQueries.php: -------------------------------------------------------------------------------- 1 | cacheMinutes, 26 | $this->cachePrefix, 27 | ); 28 | } 29 | 30 | /** 31 | * Boot the trait. 32 | * 33 | * This method registers event handlers for individual model operations that trigger Eloquent events: 34 | * - created: When a new model is created via Model::create() or $model->save() on a new instance 35 | * - updated: When an existing model is updated via $model->save() or $model->update() 36 | * - saved: When a model is created or updated via $model->save() 37 | * - deleted: When a model is deleted via $model->delete() 38 | * - restored: When a soft-deleted model is restored via $model->restore() 39 | * 40 | * NOTE: Mass operations that don't retrieve models first (like Model::where(...)->update() or 41 | * Model::where(...)->delete()) do not trigger these events. For these operations, the CacheableBuilder 42 | * class overrides methods like update(), delete(), insert(), insertGetId(), insertOrIgnore(), 43 | * updateOrInsert(), upsert(), truncate(), increment(), decrement(), forceDelete(), and restore() 44 | * to ensure cache is properly invalidated in all scenarios. 45 | * 46 | * @return void 47 | */ 48 | public static function bootHasCachedQueries() 49 | { 50 | // Flush the cache when a model is created 51 | static::created(function ($model) { 52 | static::flushModelCache(); 53 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 54 | logger()->info("Cache flushed after creation for model: " . get_class($model)); 55 | } 56 | }); 57 | 58 | // Flush the cache when a model is updated 59 | static::updated(function ($model) { 60 | static::flushModelCache(); 61 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 62 | logger()->info("Cache flushed after update for model: " . get_class($model)); 63 | } 64 | }); 65 | 66 | // Flush the cache when a model is saved 67 | static::saved(function ($model) { 68 | static::flushModelCache(); 69 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 70 | logger()->info("Cache flushed after update for model: " . get_class($model)); 71 | } 72 | }); 73 | 74 | // Flush the cache when a model is deleted 75 | static::deleted(function ($model) { 76 | static::flushModelCache(); 77 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 78 | logger()->info("Cache flushed after deletion for model: " . get_class($model)); 79 | } 80 | }); 81 | 82 | // Flush the cache when a model is restored 83 | static::registerModelEvent('restored', function ($model) { 84 | static::flushModelCache(); 85 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 86 | logger()->info("Cache flushed after restoration for model: " . get_class($model)); 87 | } 88 | }); 89 | } 90 | 91 | /** 92 | * Static method to flush cache for the model. 93 | * This allows calling Model::flushModelCache() directly without an instance. 94 | * 95 | * @return bool 96 | */ 97 | public static function flushModelCache() 98 | { 99 | try { 100 | // Get model info without creating a full instance 101 | $modelClass = static::class; 102 | $model = new static; 103 | $tableName = $model->getTable(); 104 | 105 | // Get the cache driver directly 106 | $cacheStore = config('model-cache.cache_store'); 107 | $cache = $cacheStore ? \Illuminate\Support\Facades\Cache::store($cacheStore) : \Illuminate\Support\Facades\Cache::store(); 108 | 109 | // Set tags for this model 110 | $tags = [ 111 | 'model_cache', 112 | $modelClass, 113 | $tableName 114 | ]; 115 | 116 | // Try with tags if supported 117 | if (method_exists($cache, 'tags') && $cache->supportsTags()) { 118 | try { 119 | $result = $cache->tags($tags)->flush(); 120 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 121 | logger()->info("Cache flushed statically for model: " . $modelClass); 122 | } 123 | return $result; 124 | } catch (\Exception $e) { 125 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 126 | logger()->error("Error flushing cache with tags for model {$modelClass}: " . $e->getMessage()); 127 | } 128 | // Continue to fallback method if tags fail 129 | } 130 | } 131 | 132 | // Fallback to flush the entire cache 133 | $result = $cache->flush(); 134 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 135 | logger()->info("Entire cache flushed for model: " . $modelClass); 136 | } 137 | return $result; 138 | 139 | } catch (\Exception $e) { 140 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 141 | logger()->error("Error in flushCacheStatic for model " . static::class . ": " . $e->getMessage()); 142 | } 143 | return false; 144 | } 145 | } 146 | 147 | /** 148 | * Allow flushing specific query cache when used directly in a query chain. 149 | * This method is intended to be used as: 150 | * Model::where('condition', $value)->flushCache(); 151 | * 152 | * @return bool 153 | */ 154 | public function scopeFlushCache($query) 155 | { 156 | if (method_exists($query, 'flushQueryCache')) { 157 | return $query->flushQueryCache(); 158 | } 159 | 160 | // Fallback to flushing the entire model cache 161 | return $this->flushCache(); 162 | } 163 | 164 | 165 | /** 166 | * Flush the cache for this model. 167 | * 168 | * @return bool 169 | */ 170 | public function flushCache() 171 | { 172 | try { 173 | $cache = $this->getCacheDriver(); 174 | $tags = [ 175 | 'model_cache', 176 | static::class, 177 | $this->getTable() 178 | ]; 179 | 180 | // Debug info 181 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 182 | logger()->debug("Attempting to flush cache for model: " . static::class . " (Table: " . $this->getTable() . ")"); 183 | } 184 | 185 | // First try with tags if supported 186 | if ($this->supportsTags($cache)) { 187 | try { 188 | $result = $cache->tags($tags)->flush(); 189 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 190 | logger()->info("Cache cleared with tags for model: " . static::class); 191 | } 192 | return $result; 193 | } catch (\Exception $e) { 194 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 195 | logger()->warning("Error using tags to flush cache: " . $e->getMessage() . ". Falling back to full cache clear."); 196 | } 197 | // Continue to fallback method 198 | } 199 | } 200 | 201 | // For simplicity and to ensure it actually clears the cache, 202 | // flush the entire cache when tags aren't supported 203 | $result = $cache->flush(); 204 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 205 | logger()->info("Entire cache flushed for model: " . static::class); 206 | } 207 | return $result; 208 | 209 | } catch (\Exception $e) { 210 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 211 | logger()->error("Error in flushCache: " . $e->getMessage()); 212 | } 213 | return false; 214 | } 215 | } 216 | 217 | /** 218 | * Get a static instance of the cache driver. 219 | * This allows static methods to use the cache without creating a full model instance. 220 | * 221 | * @return \Illuminate\Contracts\Cache\Repository 222 | */ 223 | protected static function getStaticCacheDriver() 224 | { 225 | try { 226 | $cacheStore = config('model-cache.cache_store'); 227 | 228 | if ($cacheStore) { 229 | return \Illuminate\Support\Facades\Cache::store($cacheStore); 230 | } 231 | 232 | return \Illuminate\Support\Facades\Cache::store(); 233 | } catch (\Exception $e) { 234 | // If there's an issue with the configured cache driver, 235 | // fall back to the default driver 236 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 237 | logger()->error('Error getting cache driver: ' . $e->getMessage()); 238 | } 239 | return \Illuminate\Support\Facades\Cache::store(config('cache.default')); 240 | } 241 | } 242 | 243 | /** 244 | * Determine if cache driver supports tags. 245 | * 246 | * @param \Illuminate\Contracts\Cache\Repository $cache 247 | * @return bool 248 | */ 249 | protected function supportsTags($cache) 250 | { 251 | try { 252 | return method_exists($cache, 'tags') && $cache->supportsTags(); 253 | } catch (\Exception $e) { 254 | return false; 255 | } 256 | } 257 | 258 | /** 259 | * Get the cache driver to use. 260 | * 261 | * @return Repository 262 | */ 263 | protected function getCacheDriver() 264 | { 265 | try { 266 | $cacheStore = config('model-cache.cache_store'); 267 | 268 | if ($cacheStore) { 269 | return \Illuminate\Support\Facades\Cache::store($cacheStore); 270 | } 271 | 272 | return \Illuminate\Support\Facades\Cache::store(); 273 | } catch (\Exception $e) { 274 | // If there's an issue with the configured cache driver, 275 | // fall back to the default driver 276 | return \Illuminate\Support\Facades\Cache::store(config('cache.default')); 277 | } 278 | } 279 | } 280 | -------------------------------------------------------------------------------- /src/ModelCacheServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__.'/../config/model-cache.php' => config_path('model-cache.php'), 19 | ], 'config'); 20 | 21 | if ($this->app->runningInConsole()) { 22 | $this->commands([ 23 | ClearModelCacheCommand::class, 24 | ]); 25 | } 26 | } 27 | 28 | /** 29 | * Register the application services. 30 | * 31 | * @return void 32 | */ 33 | public function register() 34 | { 35 | $this->mergeConfigFrom( 36 | __DIR__.'/../config/model-cache.php', 'model-cache' 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/ModelRelationships.php: -------------------------------------------------------------------------------- 1 | newRelatedInstance($related); 38 | 39 | $foreignPivotKey = $foreignPivotKey ?: $this->getForeignKey(); 40 | $relatedPivotKey = $relatedPivotKey ?: $instance->getForeignKey(); 41 | 42 | // Determine the relationship name if not provided 43 | if (is_null($relation)) { 44 | $relation = $this->guessBelongsToManyRelation(); 45 | } 46 | 47 | // Generate table name if not provided 48 | if (is_null($table)) { 49 | $table = $this->joiningTable($related); 50 | } 51 | 52 | // Create our caching BelongsToMany relationship 53 | return new CachingBelongsToMany( 54 | $instance->newQuery(), 55 | $this, 56 | $table, 57 | $foreignPivotKey, 58 | $relatedPivotKey, 59 | $parentKey ?: $this->getKeyName(), 60 | $relatedKey ?: $instance->getKeyName(), 61 | $relation, 62 | $this 63 | ); 64 | } 65 | 66 | /** 67 | * Override the belongsToMany relation's sync method to flush cache. 68 | * 69 | * @param string $relation 70 | * @param array $ids 71 | * @param bool $detaching 72 | * @return array 73 | */ 74 | public function syncRelationshipAndFlushCache($relation, array $ids, $detaching = true) 75 | { 76 | if (!method_exists($this, $relation)) { 77 | throw new \BadMethodCallException("Method {$relation} does not exist."); 78 | } 79 | 80 | $result = $this->$relation()->sync($ids, $detaching); 81 | 82 | // Flush the cache 83 | if (method_exists($this, 'flushModelCache')) { 84 | $this->flushModelCache(); 85 | } else { 86 | if (method_exists($this->cacheableParent, 'flushCache')) { 87 | $this->cacheableParent->flushCache(); 88 | } else { 89 | throw new \Exception('The parent model must have a flushCache() or flushModelCache() method defined. Make sure your model uses the HasCachedQueries trait. The ModelRelationships trait should be used in conjunction with the HasCachedQueries trait. See the documentation for more information.'); 90 | } 91 | } 92 | 93 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 94 | logger()->info("Cache flushed after detach operation for model: " . get_class($this)); 95 | } 96 | 97 | return $result; 98 | } 99 | 100 | /** 101 | * Override the belongsToMany relation's attach method to flush cache. 102 | * 103 | * @param string $relation 104 | * @param mixed $ids 105 | * @param array $attributes 106 | * @param bool $touch 107 | * @return void 108 | */ 109 | public function attachRelationshipAndFlushCache($relation, $ids, array $attributes = [], $touch = true) 110 | { 111 | if (!method_exists($this, $relation)) { 112 | throw new \BadMethodCallException("Method {$relation} does not exist."); 113 | } 114 | 115 | $this->$relation()->attach($ids, $attributes, $touch); 116 | 117 | // Flush the cache 118 | if (method_exists($this, 'flushModelCache')) { 119 | $this->flushModelCache(); 120 | } else { 121 | if (method_exists($this->cacheableParent, 'flushCache')) { 122 | $this->cacheableParent->flushCache(); 123 | } else { 124 | throw new \Exception('The parent model must have a flushCache() or flushModelCache() method defined. Make sure your model uses the HasCachedQueries trait. The ModelRelationships trait should be used in conjunction with the HasCachedQueries trait. See the documentation for more information.'); 125 | } 126 | } 127 | 128 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 129 | logger()->info("Cache flushed after detach operation for model: " . get_class($this)); 130 | } 131 | } 132 | 133 | /** 134 | * Override the belongsToMany relation's detach method to flush cache. 135 | * 136 | * @param string $relation 137 | * @param mixed $ids 138 | * @param bool $touch 139 | * @return int 140 | */ 141 | public function detachRelationshipAndFlushCache($relation, $ids = null, $touch = true) 142 | { 143 | if (!method_exists($this, $relation)) { 144 | throw new \BadMethodCallException("Method {$relation} does not exist."); 145 | } 146 | 147 | $result = $this->$relation()->detach($ids, $touch); 148 | 149 | // Flush the cache 150 | if (method_exists($this, 'flushModelCache')) { 151 | $this->flushModelCache(); 152 | } else { 153 | if (method_exists($this->cacheableParent, 'flushCache')) { 154 | $this->cacheableParent->flushCache(); 155 | } else { 156 | throw new \Exception('The parent model must have a flushCache() or flushModelCache() method defined. Make sure your model uses the HasCachedQueries trait. The ModelRelationships trait should be used in conjunction with the HasCachedQueries trait. See the documentation for more information.'); 157 | } 158 | } 159 | 160 | if (config('model-cache.debug_mode', false) && function_exists('logger')) { 161 | logger()->info("Cache flushed after detach operation for model: " . get_class($this)); 162 | } 163 | 164 | return $result; 165 | } 166 | 167 | /** 168 | * Get the relationship name from the backtrace. 169 | * 170 | * @return string 171 | */ 172 | protected function guessBelongsToManyRelation() 173 | { 174 | list($one, $two, $caller) = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3); 175 | 176 | return $caller['function']; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/.gitignore: -------------------------------------------------------------------------------- 1 | # Keep these directories 2 | !.gitignore 3 | --------------------------------------------------------------------------------