├── LICENSE ├── README.md ├── composer.json ├── read-aside-cache.png ├── read-through-cache.png ├── src ├── BaseRepository.php └── RepositoryInterface.php ├── write-back-cache.png └── write-through-cache.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Daniel Spadafora 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 all 13 | 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 THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Laravel Model Repository 2 | ======================== 3 | 4 | [![Latest Stable Version](https://img.shields.io/github/v/release/krazydanny/laravel-repository?include_prereleases)](https://packagist.org/packages/krazydanny/laravel-repository) [![Donate](https://img.shields.io/badge/donate-paypal-blue.svg)](https://paypal.me/danielspadafora) [![License](https://img.shields.io/github/license/krazydanny/laravel-repository)](https://github.com/krazydanny/laravel-repository/blob/master/LICENSE) 5 | 6 | 7 | 8 | This package provides an abstraction layer for easily implementing industry-standard caching strategies with Eloquent models. 9 | 10 | 11 | - [Laravel Model Repository](#laravel-model-repository) 12 | - [Main Advantages](#main-advantages) 13 | - [Simplify caching strategies and buy time](#simplify-caching-strategies-and-buy-time) 14 | - [Save cache storage and money](#save-cache-storage-and-money) 15 | - [Installation](#installation) 16 | - [Laravel version Compatibility](#laravel-version-compatibility) 17 | - [Lumen version Compatibility](#lumen-version-compatibility) 18 | - [Install the package via Composer](#install-the-package-via-composer) 19 | - [Creating a Repository for a Model](#creating-a-repository-for-a-model) 20 | - [Use with Singleton Pattern](#use-with-singleton-pattern) 21 | - [Eloquent like methods](#eloquent-like-methods) 22 | - [Making Eloquent Queries](#making-eloquent-queries) 23 | - [Caching methods overview](#caching-methods-overview) 24 | - [Implementing Caching Strategies](#implementing-caching-strategies) 25 | - [Read-Aside](#read-aside-cache) 26 | - [Read-Through](#read-through-cache) 27 | - [Write-Through](#write-through-cache) 28 | - [Write-Back](#write-back-cache) 29 | - [Pretty Queries](#pretty-queries) 30 | - [Cache Invalidation Techniques](#cache-invalidation-techniques) 31 | - [Saving cache storage](#saving-cache-storage) 32 | - [Keeping cache consistency](#keeping-cache-consistency) 33 | - [Exception handling](#exception-handling) 34 | - [Database Exceptions](#database-exceptions) 35 | - [Cache Store Exceptions](#cache-store-exceptions) 36 | - [Repository Events](#repository-events) 37 | - [Some things I wish somebody told me before](#some-things-i-wish-somebody-told-me-before) 38 | - [Bibliography](#bibliography) 39 | 40 | 41 |
42 | 43 | Main Advantages 44 | --------------- 45 | 46 | 47 | ### Simplify caching strategies and buy time 48 | 49 | Implementing high availability and concurrency caching strategies could be a complex and time consuming task without the appropiate abstraction layer. 50 | 51 | Laravel Model Repository simplifies caching strategies using human-readable chained methods for your existing Eloquent models :) 52 | 53 | 54 | ### Save cache storage and money 55 | 56 | Current available methods for caching Laravel models store the entire PHP object in cache. That consumes a lot of extra storage and results in slower response times, therefore having a more expensive infrastructure. 57 | 58 | Laravel Model Repository stores only the business specific data of your model in order to recreate exactly the same instance later (after data being loaded from cache). Saving more than 50% of cache storage and significantly reducing response times from the cache server. 59 | 60 |
61 | 62 | Installation 63 | ------------ 64 | Make sure you have properly configured a cache connection and driver in your Laravel/Lumen project. You can find cache configuration instructions for Laravel at https://laravel.com/docs/7.x/cache and for Lumen at https://lumen.laravel.com/docs/6.x/cache 65 | 66 | 67 | ### Laravel version Compatibility 68 | 69 | Laravel | Package 70 | :---------|:---------- 71 | 5.6.x | 1.2.0 72 | 5.7.x | 1.2.0 73 | 5.8.x | 1.2.0 74 | 6.x | 1.2.0 75 | 7.x | 1.2.0 76 | 77 | 78 | ### Lumen version Compatibility 79 | 80 | Lumen | Package 81 | :---------|:---------- 82 | 5.6.x | 1.2.0 83 | 5.7.x | 1.2.0 84 | 5.8.x | 1.2.0 85 | 6.x | 1.2.0 86 | 7.x | 1.2.0 87 | 88 | 89 | 90 | ### Install the package via Composer 91 | 92 | ```bash 93 | $ composer require krazydanny/laravel-repository 94 | ``` 95 | 96 |
97 | 98 | Creating a Repository for a Model 99 | --------------------------------- 100 | 101 | In order to simplify caching strategies we will encapsulate model access within a model repository. 102 | 103 | Two parameters can be passed to the constructor. The first parameter (required) is the model's full class name. The second parameter (optional) is the prefix to be used in cache to store model data. 104 | 105 | 106 | ```php 107 | namespace App\Repositories; 108 | 109 | use App\User; 110 | use KrazyDanny\Laravel\Repository\BaseRepository; 111 | 112 | class UserRepository extends BaseRepository { 113 | 114 | public function __construct ( ) { 115 | 116 | parent::__construct( 117 | User::class, // Model's full class name 118 | 'Users' // OPTIONAL the name of the cache prefix. The short class name will be used by default. In this case would be 'User' 119 | ); 120 | } 121 | } 122 | 123 | ``` 124 | 125 |
126 | 127 | 128 | Use with Singleton Pattern 129 | -------------------------- 130 | 131 | As a good practice to improve performance and keep your code simple is strongly recommended to use repositories along with the singleton pattern, avoiding the need for creating separate instances for the same repository at different project levels. 132 | 133 | First register the singleton call in a service provider: 134 | 135 | ```php 136 | namespace App\Providers; 137 | 138 | use App\Repositories\UserRepository; 139 | use Illuminate\Support\ServiceProvider; 140 | 141 | class AppServiceProvider extends ServiceProvider { 142 | 143 | public function register ( ) { 144 | 145 | $this->app->singleton( 146 | UserRepository::class, 147 | function () { 148 | return (new UserRepository); 149 | } 150 | ); 151 | } 152 | 153 | # other service provider methods here 154 | } 155 | 156 | ``` 157 | 158 | Add a line like this on every file you call the repository in order to keep code clean and pretty ;) 159 | 160 | ```php 161 | use App\Repositories\UserRepository; 162 | 163 | ``` 164 | 165 | Then access the same repository instance anywhere in your project :) 166 | 167 | ```php 168 | $userRepository = app( UserRepository::class ); 169 | 170 | ``` 171 | 172 | You can also typehint it as a parameter in controllers, event listeners, middleware or any other service class and laravel will automatically inject the repository instance 173 | 174 | ```php 175 | namespace App\Http\Controllers; 176 | 177 | use Illuminate\Http\Request; 178 | use App\Repositories\UserRepository; 179 | 180 | class UserController extends Controller 181 | { 182 | public function myMethod( UserRepository $userRepository, $id){ 183 | // you can now use the repository to work with cached models 184 | $user = $userRepository->get( $id ); 185 | } 186 | } 187 | ``` 188 |
189 | 190 | Eloquent like methods 191 | --------------------- 192 | 193 | Calling Eloquent-like methods directly from our repository gives us the advantage of combining them with caching strategies. First, let's see how we call them. It's pretty straightforward :) 194 | 195 | ### create() 196 | 197 | Create a new model: 198 | ```php 199 | $user = app( UserRepository::class )->create([ 200 | 'firstname' => 'Krazy', 201 | 'lastname' => 'Danny', 202 | 'email' => 'somecrazy@email.com', 203 | 'active' => true, 204 | ]); 205 | 206 | $user_id = $user->getKey(); 207 | 208 | ``` 209 | 210 | ### get() 211 | 212 | Get a specific model by ID: 213 | ```php 214 | $user = app( UserRepository::class )->get( $user_id ); 215 | 216 | ``` 217 | 218 | ### save() 219 | 220 | Update a specific model: 221 | ```php 222 | $user->active = false; 223 | 224 | app( UserRepository::class )->save( $user ); 225 | 226 | ``` 227 | 228 | ### delete() 229 | 230 | Delete a specific model: 231 | ```php 232 | app( UserRepository::class )->delete( $user ); 233 | 234 | ``` 235 | 236 |
237 | 238 | 239 | Making Eloquent Queries 240 | ----------------------- 241 | 242 | Unlike get() or save(), query methods work a little different. They receive as parameter the desired query builder instance (Illuminate\Database\Eloquent\Builder) in order to execute the query. 243 | 244 | This will allow us to combine queries with caching strategies, as we will cover forward on this document. For now let's focus on the query methods only. For example: 245 | 246 | ### find() 247 | 248 | To find all models under a certain criteria: 249 | ```php 250 | $q = User::where( 'active', true ); 251 | 252 | $userCollection = app( UserRepository::class )->find( $q ); 253 | 254 | ``` 255 | 256 | ### first() 257 | 258 | To get the first model instance under a certain criteria: 259 | ```php 260 | $q = User::where( 'active', true ); 261 | 262 | $user = app( UserRepository::class )->first( $q ); 263 | 264 | ``` 265 | 266 | ### count() 267 | 268 | To count all model instances under a certain criteria: 269 | ```php 270 | $q = User::where( 'active', true ); 271 | 272 | $userCount = app( UserRepository::class )->count( $q ); 273 | 274 | ``` 275 | 276 |
277 | 278 | 279 | Caching methods overview 280 | ------------------------ 281 | 282 | ### remember() & during() 283 | 284 | Calling remember() before any query method like find(), first() or count() stores the query result in cache for a given time. Always followed by the during() method, which defines the duration of the results in cache (TTL/Time-To-Live in seconds) 285 | 286 | **VERY IMPORTANT:** For Laravel/Lumen v5.7 and earlier versions TTL param passed to during() are minutes instead of seconds. This library follows Laravel standards so check what unit of time your version uses for the Cache facade. 287 | 288 | 289 | ```php 290 | $q = User::where( 'active', true ); 291 | 292 | app( UserRepository::class )->remember()->during( 3600 )->find( $q ); 293 | 294 | ``` 295 | 296 | 297 | Also a model instance could be passed as parameter in order to store that specific model in cache. 298 | 299 | 300 | ```php 301 | app( UserRepository::class )->remember( $user )->during( 3600 ); 302 | 303 | ``` 304 | 305 | 306 | ### according() 307 | 308 | The according() method does almost the same as during() but with a difference, it reads the time-to-live in seconds from a given model's attribute: 309 | 310 | ```php 311 | app( ReservationsRepository::class )->remember( $reservation )->according( 'expiresIn' ); 312 | 313 | ``` 314 | 315 | This is useful if different instances of the same class have/need different or dynamic time-to-live values. 316 | 317 | 318 | 319 | ### rememberForever() 320 | 321 | Calling rememberForever() before any query method like find(), first() or count() stores the query result in cache without an expiration time. 322 | 323 | 324 | ```php 325 | $q = User::where( 'active', true ); 326 | 327 | app( UserRepository::class )->rememberForever()->find( $q ); 328 | 329 | ``` 330 | 331 | 332 | Also a model instance could be passed as parameter in order to store that specific model in cache without expiration. 333 | 334 | 335 | ```php 336 | app( UserRepository::class )->rememberForever( $user ); 337 | 338 | ``` 339 | 340 | 341 | ### fromCache() 342 | 343 | Calling fromCache() before any query method like find(), first() or count() will try to retrieve the results from cache ONLY. 344 | 345 | 346 | ```php 347 | $q = User::where( 'active', true ); 348 | 349 | app( UserRepository::class )->fromCache()->find( $q ); 350 | 351 | ``` 352 | 353 | 354 | Also a model instance could be passed as parameter in order to retrieve that specific model from cache ONLY. 355 | 356 | 357 | ```php 358 | app( UserRepository::class )->fromCache( $user ); 359 | 360 | ``` 361 | 362 | 363 | ### forget() 364 | 365 | This method removes one or many models (or queries) from cache. It's very useful when you have updated models in the database and need to invalidate cached model data or related query results (for example: to have real-time updated cache). 366 | 367 | The first parameter must be an instance of the model, a specific model ID (primary key) or a query builder instance (Illuminate\Database\Eloquent\Builder). 368 | 369 | 370 | Forget query results: 371 | ```php 372 | $query = User::where( 'active', true ); 373 | 374 | app( UserRepository::class )->forget( $query ); 375 | 376 | ``` 377 | 378 | Forget a specific model using the object: 379 | ```php 380 | app( UserRepository::class )->forget( $userModelInstance ); 381 | 382 | ``` 383 | 384 | Forget a specific model by id: 385 | ```php 386 | app( UserRepository::class )->forget( $user_id ); 387 | 388 | ``` 389 | 390 | The second parameter (optional) could be an array to queue forget() operations in order to be done in a single request to the cache server. 391 | 392 | When passed the forget() method appends to the array (by reference) the removal operations instead of sending them instantly to the cache server. 393 | 394 | It's useful when you need to expire many cached queries or models of the same repository. You can do it in one request optimizing response times for your cache server, therefore your app :) 395 | 396 | For example: 397 | ```php 398 | $user->active = false; 399 | $user->save(); 400 | 401 | $forgets = []; 402 | 403 | #removes user model from cache 404 | app( UserRepository::class )->forget( $user, $forgets ); 405 | 406 | #removes query that finds active users 407 | $query = User::where( 'active', true ); 408 | app( UserRepository::class )->forget( $query, $forgets ); 409 | 410 | #requests all queued removals to the cache server 411 | app( UserRepository::class )->forget( $forgets ); 412 | 413 | ``` 414 | 415 |
416 | 417 | 418 | Implementing Caching Strategies 419 | ------------------------------- 420 | 421 |
422 | 423 | ### Read-Aside Cache 424 | 425 |

426 | Read Aside Caching 427 |

428 | 429 | **How it works?** 430 | 431 | 1. The app first looks the desired model or query in the cache. If the data was found in cache, we’ve cache hit. The model or query results are read and returned to the client without database workload at all. 432 | 2. If model or query results were not found in cache we have a cache miss, then data is retrieved from database. 433 | 3. Model or query results retrived from database are stored in cache in order to have a successful cache hit next time. 434 | 435 | **Use cases** 436 | 437 | Works best for heavy read workload scenarios and general purpose. 438 | 439 | **Pros** 440 | 441 | Provides balance between lowering database read workload and cache storage use. 442 | 443 | **Cons** 444 | 445 | In some cases, to keep cache up to date in real-time, you may need to implement cache invalidation using the forget() method. 446 | 447 | **Usage** 448 | 449 | 450 | When detecting you want a model or query to be remembered in cache for a certain period of time, Laravel Model Repository will automatically first try to retrieve it from cache. Otherwise will automatically retrieve it from database and store it in cache for the next time :) 451 | 452 | 453 | Read-Aside a specific model by ID: 454 | ```php 455 | $user = app( UserRepository::class )->remember()->during( 3600 )->get( $user_id ); 456 | 457 | ``` 458 | 459 | Read-Aside query results: 460 | ```php 461 | $q = User::where( 'active', true ); 462 | 463 | $userCollection = app( UserRepository::class )->remember()->during( 3600 )->find( $q ); 464 | 465 | $userCount = app( UserRepository::class )->remember()->during( 3600 )->count( $q ); 466 | 467 | $firstUser = app( UserRepository::class )->remember()->during( 3600 )->first( $q ); 468 | 469 | ``` 470 |
471 | 472 | ### Read-Through Cache 473 | 474 |

475 | Read Through Caching 476 |

477 | 478 | **How it works?** 479 | 480 | 1. The app first looks the desired model or query in the cache. If the data was found in cache, we’ve cache hit. The model or query results are read and returned to the client without database workload at all. 481 | 2. If model or query results were not found in cache we have a cache miss, then data is retrieved from database ONLY THIS TIME in order to be always available from cache. 482 | 483 | 484 | **Use cases** 485 | 486 | Works best for heavy read workload scenarios where the same model or query is requested constantly. 487 | 488 | **Pros** 489 | 490 | Keeps database read workload at minimum because always retrieves data from cache. 491 | 492 | **Cons** 493 | 494 | If you want cache to be updated you must combine with Write-Through strategy (incrementing writes latency and workload in some cases) or implementing cache invalidation using the forget() method. 495 | 496 | 497 | **Usage** 498 | 499 | 500 | When detecting you want a model or query to be remembered in cache forever, Laravel Model Repository will automatically first try to retrieve it from cache. Otherwise will automatically retrieve it from database and store it without expiration, so it will be always available form cache :) 501 | 502 | 503 | Read-Through a specific model by ID: 504 | ```php 505 | $user = app( UserRepository::class )->rememberForever()->get( $user_id ); 506 | 507 | ``` 508 | 509 | Read-Through query results: 510 | ```php 511 | $q = User::where( 'active', true ); 512 | 513 | $userCollection = app( UserRepository::class )->rememberForever()->find( $q ); 514 | 515 | $userCount = app( UserRepository::class )->rememberForever()->count( $q ); 516 | 517 | $firstUser = app( UserRepository::class )->rememberForever()->first( $q ); 518 | 519 | ``` 520 |
521 | 522 | ### Write-Through Cache 523 | 524 |

525 | Write Through Caching 526 |

527 | 528 | **How it works?** 529 | 530 | Models are always stored in cache and database. 531 | 532 | 533 | **Use cases** 534 | 535 | Used in scenarios where consistency is a priority or needs to be granted. 536 | 537 | 538 | **Pros** 539 | 540 | No cache invalidation techniques required. No need for using forget() method. 541 | 542 | **Cons** 543 | 544 | Could introduce write latency in some scenarios because data is always written in cache and database. 545 | 546 | 547 | **Usage** 548 | 549 | When detecting you want a model to be remembered in cache, Laravel Model Repository will automatically store it in cache and database (inserting or updating depending on the case). 550 | 551 | 552 | Write-Through without expiration time: 553 | ```php 554 | # create a new user in cache and database 555 | $user = app( UserRepository::class )->rememberForever()->create([ 556 | 'firstname' => 'Krazy', 557 | 'lastname' => 'Danny', 558 | 'email' => 'somecrazy@email.com', 559 | 'active' => true, 560 | ]); 561 | 562 | # update an existing user in cache and database 563 | $user->active = false; 564 | 565 | app( UserRepository::class )->rememberForever()->save( $user ); 566 | 567 | ``` 568 | 569 | Write-Through with expiration time (TTL): 570 | ```php 571 | # create a new user in cache and database 572 | $user = app( UserRepository::class )->remember()->during( 3600 )->create([ 573 | 'firstname' => 'Krazy', 574 | 'lastname' => 'Danny', 575 | 'email' => 'somecrazy@email.com', 576 | 'active' => true, 577 | ]); 578 | 579 | # update an existing user in cache and database 580 | $user->active = false; 581 | 582 | app( UserRepository::class )->remember()->during( 3600 )->save( $user ); 583 | 584 | ``` 585 |
586 | 587 | ### Write-Back Cache 588 | 589 |

590 | Write Back Caching 591 |

592 | 593 | **How it works?** 594 | 595 | Models are stored only in cache until they are massively persisted in database. 596 | 597 | 598 | **Use cases** 599 | 600 | Used in heavy write load scenarios and database-cache consistency is not a priority. 601 | 602 | 603 | **Pros** 604 | 605 | Very performant and resilient to database failures and downtimes 606 | 607 | **Cons** 608 | 609 | In some cache failure scenarios data may be permanently lost. 610 | 611 | 612 | **Usage** 613 | 614 | *IMPORTANT!! THIS STRATEGY IS AVAILABLE FOR REDIS CACHE STORES ONLY (at the moment)* 615 | 616 | With the buffer() or index() method Laravel Model Repository will store data in cache untill you call the persist() method which will iterate many (batch) of cached models at once, allowing us to persist them the way our project needs through a callback function. 617 | 618 | 619 | First write models in cache: 620 | 621 | Using **buffer()** 622 | 623 | Stores models in cache in a way only accesible within the persist() method callback. Useful for optimizing performance and storage when you don't need to access them until they are persisted in database. 624 | 625 | ```php 626 | $model = app( TransactionsRepository::class )->buffer( new Transactions( $data ) ); 627 | 628 | ``` 629 | 630 | Using **index()** 631 | 632 | Stores models in a way that they are available to be loaded from cache by get() method too. Useful when models need to be accesible before they are persisted. 633 | 634 | ```php 635 | $model = app( TransactionsRepository::class )->index( new Transactions( $data ) ); 636 | 637 | ``` 638 | 639 | Then massively persist models in database: 640 | 641 | Using **persist()** 642 | 643 | The persist() method could be called later in a separate job or scheduled task, allowing us to manage how often we need to persist models into the database depending on our project's traffic and infrastructure. 644 | 645 | 646 | ```php 647 | app( TransactionsRepository::class )->persist( 648 | 649 | // the first param is a callback which returns true if models were persisted successfully, false otherwise 650 | function( $collection ) { 651 | 652 | foreach ( $collection as $model ) { 653 | 654 | // do database library custom and optimized logic here 655 | 656 | // for example: you could use bulk inserts and transactions in order to improve both performance and consistency 657 | } 658 | 659 | if ( $result ) 660 | return true; // if true remove model ids from persist() queue 661 | 662 | return false; // if false keeps model ids in persist() queue and tries again next time persist() method is called 663 | }, 664 | 665 | // the second param (optional) is an array with one or many of the following available options 666 | [ 667 | 'written_since' => 0, // process only models written since ths specified timestamp in seconds 668 | 'written_until' => \time(), // process only models written until the given timestamp in seconds 669 | 'object_limit' => 500, // the object limit to be processed at the same time (to prevent memory overflows) 670 | 'clean_cache' => true, // if true and callback returns true, marks models as persisted 671 | 'method' => 'buffer' // buffer | index 672 | ] 673 | ); 674 | 675 | ``` 676 | 677 | The **method** parameter: 678 | 679 | It has two possible values. 680 | 681 | - **buffer** (default) 682 | 683 | Performs persist() only for those models stored in cache with the buffer() method; 684 | 685 | - **index** 686 | 687 | Performs persist() only for those models stored in cache with the index() method; 688 | 689 | 690 |
691 | 692 | Pretty Queries 693 | ---------------------------------- 694 | 695 | You can create human readable queries that represent your business logic in an intuititve way and ensures query criteria consistency encapsulating it's code. 696 | 697 | For example: 698 | 699 | ```php 700 | namespace App\Repositories; 701 | 702 | use App\User; 703 | use KrazyDanny\Laravel\Repository\BaseRepository; 704 | 705 | class UserRepository extends BaseRepository { 706 | 707 | public function __construct ( ) { 708 | 709 | parent::__construct( 710 | User::class, // Model's class name 711 | 'Users' // the name of the cache prefix 712 | ); 713 | } 714 | 715 | public function findByState ( string $state ) { 716 | 717 | return $this->find( 718 | User::where([ 719 | 'state' => $state, 720 | 'deleted_at' => null, 721 | ]) 722 | ); 723 | } 724 | 725 | } 726 | 727 | ``` 728 | 729 | Then call a pretty query :) 730 | 731 | ```php 732 | $activeUsers = app( UserRepository::class )->findByState( 'active' ); 733 | 734 | $activeUsers = app( UserRepository::class )->remember()->during( 3600 )->findByState( 'active' ); 735 | 736 | $activeUsers = app( UserRepository::class )->rememberForever()->findByState( 'active' ); 737 | 738 | ``` 739 | 740 |
741 | 742 | Cache invalidation techniques 743 | ------------------------------------------ 744 | 745 | In some cases we will need to remove models or queries from cache even if we've set an expiration time for them. 746 | 747 | ### Saving cache storage 748 | 749 | To save storage we need data to be removed from cache, so we'll use the forget() method. Remember? 750 | 751 | 752 | **For specific models:** 753 | ```php 754 | app( UserRepository::class )->forget( $user ); 755 | 756 | ``` 757 | **For queries:** 758 | 759 | ```php 760 | $user->active = false; 761 | $user->save(); 762 | 763 | $query = User::where( 'active', true ); 764 | app( UserRepository::class )->forget( $query ); 765 | 766 | ``` 767 | 768 | **On events** 769 | 770 | Now let's say we want to invalidate some specific queries when creating or updating a model. We could do something like this: 771 | 772 | ```php 773 | namespace App\Repositories; 774 | 775 | use App\User; 776 | use KrazyDanny\Laravel\Repository\BaseRepository; 777 | 778 | class UserRepository extends BaseRepository { 779 | 780 | public function __construct ( ) { 781 | 782 | parent::__construct( 783 | User::class, // Model's class name 784 | 'Users' // the name of the cache prefix 785 | ); 786 | } 787 | 788 | // then call this to invalidate active users cache and any other queries or models cache you need. 789 | public function forgetOnUserSave ( User $user ) { 790 | 791 | // let's use a queue to make only one request with all operations to the cache server 792 | $invalidations = []; 793 | 794 | // invalidates that specific user model cache 795 | $this->forget( $user, $invalidations ); 796 | 797 | // invalidates the active users query cache 798 | $this->forget( 799 | User::where([ 800 | 'state' => 'active', 801 | 'deleted_at' => null, 802 | ]), 803 | $invalidations 804 | ); 805 | 806 | // makes request to the server and invalidates all cache entries at once 807 | 808 | $this->forget( $invalidations ); 809 | } 810 | 811 | } 812 | 813 | ``` 814 | 815 | Then, in the user observer... 816 | 817 | ```php 818 | namespace App\Observers; 819 | 820 | use App\User; 821 | use App\Repositories\UserRepository; 822 | 823 | class UserObserver { 824 | 825 | public function saved ( User $model ) { 826 | 827 | app( UserRepository::class )->forgetOnUserSave( $model ); 828 | } 829 | 830 | # here other observer methods 831 | } 832 | 833 | ``` 834 | 835 | ### For real-time scenarios 836 | 837 | To keep real-time cache consistency we want model data to be updated in the cache instead of being removed. 838 | 839 | 840 | **For specific models:** 841 | 842 | We will simply use remember(), during() and rememberForever() methods: 843 | 844 | ```php 845 | app( UserRepository::class )->rememberForever( $user ); 846 | // or 847 | app( UserRepository::class )->remember( $user )->during( 3600 ); 848 | 849 | ``` 850 | 851 | **For queries:** 852 | 853 | We would keep using forget() method as always, otherwise it would be expensive anyway getting the query from the cache, updating it somehow and then overwriting cache again. 854 | 855 | **On events** 856 | 857 | Let's assume we want to update model A in cache when model B is updated. 858 | 859 | We could do something like this in the user observer: 860 | 861 | ```php 862 | namespace App\Observers; 863 | 864 | use App\UserSettings; 865 | use App\Repositories\UserRepository; 866 | 867 | class UserSettingsObserver { 868 | 869 | public function saved ( UserSettings $model ) { 870 | 871 | app( UserRepository::class )->remember( $model )->during( 3600 ); 872 | } 873 | 874 | # here other observer methods 875 | } 876 | 877 | ``` 878 | 879 |
880 | 881 | Repository Events 882 | ----------------- 883 | 884 | We can also observe the following repository-level events. 885 | 886 | - afterGet 887 | - afterFirst 888 | - afterFind 889 | - afterCount 890 | 891 | 892 | **On each call** 893 | 894 | ```php 895 | $callback = function ( $cacheHit, $result ) { 896 | 897 | if ( $cacheHit ) { 898 | // do something when the query hits the cache 899 | } 900 | else { 901 | // do something else when the query hits the database 902 | // this is not for storing the model in cache, remember the repository did it for you. 903 | } 904 | } 905 | 906 | app( UserRepository::class )->observe( $callback )->rememberForever()->get( $user_id ); 907 | 908 | ``` 909 | 910 | **On every call** 911 | 912 | ```php 913 | $callback = function ( $cacheHit, $result ) { 914 | 915 | if ( $cacheHit ) { 916 | // do something when the query hits the cache 917 | } 918 | else { 919 | // do something else when the query hits the database 920 | // this is not for storing the model in cache, remember the repository did it for you. 921 | } 922 | } 923 | 924 | app( UserRepository::class )->observeAlways( 'afterGet', $callback); 925 | 926 | app( UserRepository::class )->rememberForever()->get( $user_A_id ); 927 | 928 | app( UserRepository::class )->rememberForever()->get( $user_B_id ); 929 | 930 | ``` 931 | 932 | **Some use cases...** 933 | 934 | - Monitoring usage of our caching strategy in production environments. 935 | - Have a special treatment for models or query results loaded from cache than those retrieved from database. 936 | 937 | 938 |
939 | 940 | Exceptions handling 941 | ------------------- 942 | 943 | ### Cache Exceptions 944 | 945 | ```php 946 | app( UserRepository::class )->handleCacheExceptions(function( $e ){ 947 | // here we can do something like log the exception silently 948 | }) 949 | 950 | ``` 951 | 952 | ### Database Exceptions 953 | 954 | ```php 955 | app( UserRepository::class )->handleDatabaseExceptions(function( $e ){ 956 | // here we can do something like log the exception silently 957 | }) 958 | 959 | ``` 960 | 961 | ### The silently() method 962 | 963 | When called before any method, that operation will not throw database nor cache exceptions. Unless we've thrown them inside handleDatabaseExceptions() or handleCacheStoreExceptions() methods. 964 | 965 | For example: 966 | 967 | ```php 968 | app( UserRepository::class )->silently()->rememberForever()->get( $user_id ); 969 | 970 | ``` 971 | 972 | 973 |
974 | 975 | Some things I wish somebody told me before 976 | ------------------------------------------ 977 | 978 | ### "Be shapeless, like water my friend" (Bruce Lee) 979 | 980 | There's no unique, best or does-it-all-right caching technique. 981 | 982 | Every caching strategy has it's own advantages and disadvantages. Is up to you making a good analysis of what you project needs and it's priorities. 983 | 984 | Even in the same project you may use different caching strategies for different models. For example: Is not the same caching millons of transaction logs everyday than registering a few new users in your app. 985 | 986 | Also this library is designed to be implemented on the go. This means you can progressively apply caching techniques on specific calls. 987 | 988 | Lets say we currently have the following line in many places of our project: 989 | 990 | ```php 991 | $model = SomeModel::create( $data ); 992 | 993 | ``` 994 | 995 | Now assume we want to implement write-back strategy for that model only in some critical places of our project and see how it goes. Then we should only replace those specifice calls with: 996 | 997 | ```php 998 | $model = app( SomeModelRepository::class )->buffer( new SomeModel( $data ) ); 999 | 1000 | ``` 1001 | 1002 | And leave those calls we want out of the caching strategy alone, they are not affected at all. Besides some things doesn't really need to be cached. 1003 | 1004 | Be like water my friend... ;) 1005 | 1006 |
1007 | 1008 | Bibliography 1009 | ------------ 1010 | 1011 | Here are some articles which talk in depth about caching strategies: 1012 | 1013 | - https://bluzelle.com/blog/things-you-should-know-about-database-caching 1014 | - https://zubialevich.blogspot.com/2018/08/caching-strategies.html 1015 | - https://codeahoy.com/2017/08/11/caching-strategies-and-how-to-choose-the-right-one/ 1016 | - https://docs.aws.amazon.com/AmazonElastiCache/latest/mem-ug/BestPractices.html 1017 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "krazydanny/laravel-repository", 3 | "description": "An abstraction layer for easily implementing industry-standard caching strategies to Eloquent models.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Daniel Spadafora", 9 | "email": "danielspadafora@gmail.com" 10 | } 11 | ], 12 | "minimum-stability": "dev", 13 | "autoload": { 14 | "psr-4": { 15 | "KrazyDanny\\Laravel\\Repository\\": "src/" 16 | } 17 | }, 18 | "require": {} 19 | } -------------------------------------------------------------------------------- /read-aside-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krazydanny/laravel-repository/835d10a83301e1bebc0b3be51c80b2808303570e/read-aside-cache.png -------------------------------------------------------------------------------- /read-through-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krazydanny/laravel-repository/835d10a83301e1bebc0b3be51c80b2808303570e/read-through-cache.png -------------------------------------------------------------------------------- /src/BaseRepository.php: -------------------------------------------------------------------------------- 1 | class = $class; 40 | 41 | if ( $cache_prefix ) { 42 | 43 | $this->cachePrefix = $cache_prefix; 44 | } 45 | else { 46 | 47 | $this->detectCachePrefix(); 48 | } 49 | } 50 | 51 | 52 | protected function detectCachePrefix ( ) { 53 | 54 | $parts = \explode( '\\', $this->class ); 55 | 56 | $this->cachePrefix = \end( 57 | $parts 58 | ); 59 | } 60 | 61 | 62 | public function observeAlways ( 63 | string $event, 64 | \Closure $callback 65 | ) 66 | { 67 | switch ( $event ) { 68 | 69 | case 'afterGet': 70 | case 'afterFind': 71 | case 'afterFirst': 72 | case 'afterCount': 73 | break; 74 | default: 75 | throw new \Exception( 76 | 'Unsupported event' 77 | ); 78 | 79 | } 80 | 81 | $this->observers[$event] = $callback; 82 | } 83 | 84 | 85 | public function observe ( 86 | \Closure $callback 87 | ) 88 | { 89 | $this->observe = $callback; 90 | 91 | return $this; 92 | } 93 | 94 | 95 | protected function fireObserverEvent ( 96 | string $event, 97 | bool $cacheHit, 98 | $result 99 | ) : bool 100 | { 101 | 102 | if ( $this->observe ) 103 | $method = $this->observe; 104 | else 105 | $method = $this->observers[$event] ?? false; 106 | 107 | if ( $method ) { 108 | 109 | $method( $cacheHit, $result ); 110 | 111 | return true; 112 | } 113 | 114 | return false; 115 | } 116 | 117 | 118 | public function silently ( ) { 119 | 120 | $this->mute = true; 121 | } 122 | 123 | 124 | public function remember ( Model $model = null ) { 125 | 126 | $this->model = $model; 127 | 128 | return $this; 129 | } 130 | 131 | 132 | public function according ( string $attribute ) { 133 | 134 | $this->according = $attribute; 135 | 136 | if ( $this->model ) { 137 | 138 | $this->ttl = $this->getTTLFromAttribute( 139 | $this->model, 140 | $attribute 141 | ); 142 | 143 | $r = $this->storeModelInCache( 144 | $this->model 145 | ); 146 | 147 | $this->clearSettings(); 148 | 149 | return $r; 150 | } 151 | 152 | return $this; 153 | } 154 | 155 | 156 | protected function getTTLFromAttribute( 157 | Model $model, 158 | string $attribute 159 | ) : int 160 | { 161 | if ( $model->$attribute > 0 ) { 162 | 163 | return (int)$model->$attribute; 164 | } 165 | 166 | return 0; 167 | } 168 | 169 | 170 | public function during ( int $seconds ) { 171 | 172 | $this->ttl = $seconds; 173 | 174 | if ( $this->model ) { 175 | 176 | $r = $this->storeModelInCache( 177 | $this->model 178 | ); 179 | 180 | $this->clearSettings(); 181 | 182 | return $r; 183 | } 184 | 185 | return $this; 186 | } 187 | 188 | 189 | 190 | public function fromCache ( ) { 191 | 192 | $this->fromCache = true; 193 | return $this; 194 | } 195 | 196 | 197 | public function rememberForever ( 198 | Model $model = null 199 | ) 200 | { 201 | 202 | $this->ttl = -1; 203 | 204 | if ( $model ) { 205 | 206 | $this->index( $model ); 207 | 208 | $r = $this->storeModelInCache( $model ); 209 | 210 | $this->clearSettings(); 211 | 212 | return $r; 213 | } 214 | 215 | return $this; 216 | } 217 | 218 | 219 | public function skip ( int $entities ) { 220 | 221 | $this->skip = $entities; 222 | return $this; 223 | } 224 | 225 | 226 | public function take ( int $entities ) { 227 | 228 | $this->take = $entities; 229 | return $this; 230 | } 231 | 232 | 233 | public function get ( $id ) : ?Model { 234 | 235 | if ( !$id ) 236 | return null; 237 | 238 | if ( 239 | $this->ttl != 0 240 | || $this->fromCache 241 | || $this->according 242 | ) 243 | { 244 | 245 | try { 246 | 247 | $data = Cache::get( 248 | $this->cachePrefix.':'.$id 249 | ); 250 | } 251 | catch ( \Exception $e ) { 252 | 253 | $data = null; 254 | $this->handleCacheException( $e ); 255 | } 256 | 257 | if ( $data ) { 258 | 259 | $class = $this->class; 260 | $models = $class::hydrate([$data]); 261 | 262 | $model = $models[0] ?? null; 263 | 264 | if ( $model ) { 265 | 266 | $this->fireObserverEvent( 267 | 'afterGet', 268 | true, 269 | $model 270 | ); 271 | } 272 | 273 | $this->clearSettings(); 274 | 275 | return $model; 276 | } 277 | else if ( $this->fromCache ) { 278 | 279 | $this->fireObserverEvent( 280 | 'afterGet', 281 | false, 282 | null 283 | ); 284 | 285 | $this->clearSettings(); 286 | 287 | return null; 288 | } 289 | else { 290 | 291 | $model = $this->class::find( $id ); 292 | 293 | if ( $model ) { 294 | 295 | if ( $this->according ) { 296 | 297 | $this->ttl = $this->getTTLFromAttribute( 298 | $model, 299 | $this->according 300 | ); 301 | } 302 | 303 | $this->storeModelInCache( $model ); 304 | 305 | $this->fireObserverEvent( 306 | 'afterGet', 307 | false, 308 | $model 309 | ); 310 | } 311 | 312 | $this->clearSettings(); 313 | 314 | return $model; 315 | } 316 | } 317 | 318 | $this->clearSettings(); 319 | 320 | return $this->class::find( $id ); 321 | } 322 | 323 | 324 | protected function clearSettings () { 325 | 326 | $this->ttl = 0; 327 | $this->take = false; 328 | $this->skip = false; 329 | $this->fromCache = false; 330 | $this->mute = false; 331 | $this->according = null; 332 | $this->observe = null; 333 | } 334 | 335 | 336 | public function create ( array $attributes ) : ?Model { 337 | 338 | if ( empty( $attributes ) ) { 339 | 340 | $this->clearSettings(); 341 | return null; 342 | } 343 | 344 | $model = $this->class::create( $attributes ); 345 | 346 | if ( $model ) { 347 | 348 | if ( $this->according ) { 349 | 350 | $this->ttl = $this->getTTLFromAttribute( 351 | $model, 352 | $this->according 353 | ); 354 | } 355 | else if ( $this->ttl < 0 ) { 356 | 357 | $this->index( $model ); 358 | } 359 | 360 | $this->storeModelInCache( $model ); 361 | } 362 | 363 | $this->clearSettings(); 364 | 365 | return $model; 366 | } 367 | 368 | 369 | public function save ( Model $model ) : bool { 370 | 371 | if ( $model->save() ) { 372 | 373 | if ( $this->according ) { 374 | 375 | $this->ttl = $this->getTTLFromAttribute( 376 | $model, 377 | $this->according 378 | ); 379 | } 380 | else if ( $this->ttl < 0 ) { 381 | 382 | $this->index( $model ); 383 | } 384 | 385 | $this->storeModelInCache( $model ); 386 | $this->clearSettings(); 387 | 388 | return true; 389 | } 390 | 391 | $this->clearSettings(); 392 | 393 | return false; 394 | } 395 | 396 | 397 | public function delete ( Model $model ) : bool { 398 | 399 | if ( $this->fromCache ) { 400 | 401 | $this->forget( $model ); 402 | 403 | $this->clearSettings(); 404 | 405 | return true; 406 | } 407 | else if ( $model->delete() ) { 408 | 409 | $this->forget( $model ); 410 | 411 | $this->clearSettings(); 412 | 413 | return true; 414 | } 415 | 416 | return false; 417 | } 418 | 419 | 420 | public function first ( 421 | Builder $queryBuilder 422 | ) : ?Model 423 | { 424 | 425 | $ttl = $this->ttl; 426 | $hit = true; 427 | 428 | $data = $this->query( 429 | $queryBuilder, 430 | function () use ( $queryBuilder, $ttl, $hit ) { 431 | 432 | $hit = false; 433 | $model = $queryBuilder->first(); 434 | 435 | if ( $model ) { 436 | 437 | if ( $ttl == 0 ) 438 | return $model; 439 | 440 | return $model->toArray(); 441 | } 442 | 443 | return null; 444 | }, 445 | $this->generateQueryCacheKey( 446 | $queryBuilder 447 | ).':first' 448 | ); 449 | 450 | $this->fireObserverEvent( 451 | 'afterFirst', 452 | $hit, 453 | $data 454 | ); 455 | 456 | $this->clearSettings(); 457 | 458 | if ( is_array($data) ) { 459 | 460 | $class = $this->class; 461 | 462 | $class::unguard(); 463 | 464 | $models = $class::hydrate([$data]); 465 | 466 | $class::reguard(); 467 | 468 | return $models[0] ?? null; 469 | } 470 | 471 | return $data; 472 | } 473 | 474 | 475 | public function index ( Model $model ) { 476 | 477 | try { 478 | 479 | $store = Cache::getStore(); 480 | } 481 | catch ( \Exception $e ) { 482 | 483 | $this->handleCacheException( $e ); 484 | } 485 | 486 | if ( !$store instanceof RedisStore ) { 487 | 488 | throw new \Exception( 489 | 'buffer() is only available for the following cache stores: Illuminate\Cache\RedisStore' 490 | ); 491 | } 492 | 493 | try { 494 | 495 | $store->connection()->zadd( 496 | $this->cachePrefix.':index', 497 | \time(), 498 | $model->getKey() 499 | ); 500 | } 501 | catch ( \Exception $e ) { 502 | 503 | $this->handleCacheException( $e ); 504 | } 505 | 506 | } 507 | 508 | 509 | public function buffer ( Model $model ) { 510 | 511 | try { 512 | 513 | $store = Cache::getStore(); 514 | } 515 | catch ( \Exception $e ) { 516 | 517 | $this->handleCacheException( $e ); 518 | } 519 | 520 | if ( !$store instanceof RedisStore ) { 521 | 522 | throw new \Exception( 523 | 'buffer() is only available for the following cache stores: Illuminate\Cache\RedisStore' 524 | ); 525 | } 526 | 527 | try { 528 | 529 | $store->connection()->zadd( 530 | $this->cachePrefix.':buffer', 531 | \time(), 532 | \serialize( 533 | $model->toArray() 534 | ) 535 | ); 536 | } 537 | catch ( \Exception $e ) { 538 | 539 | $this->handleCacheException( $e ); 540 | } 541 | } 542 | 543 | 544 | protected function unserializeMulti ( 545 | array $data 546 | ) : Collection 547 | { 548 | if ( !$data ) 549 | return new Collection; 550 | 551 | $attributes = []; 552 | 553 | foreach ( $data as $d ) { 554 | 555 | if ( $d ) { 556 | $attributes[] = \unserialize( $d ); 557 | } 558 | } 559 | 560 | unset( $data ); 561 | 562 | return $this->class::hydrate( $attributes ); 563 | } 564 | 565 | 566 | protected function getIndexedModels ( 567 | int $written_since, 568 | int $written_until, 569 | int $skip, 570 | int $take 571 | ) : Collection 572 | { 573 | try { 574 | 575 | $store = Cache::getStore(); 576 | } 577 | catch ( \Exception $e ) { 578 | 579 | $this->handleCacheException( $e ); 580 | } 581 | 582 | if ( !$store instanceof RedisStore ) { 583 | 584 | throw new \Exception( 585 | 'persist() is only available for the following cache stores: Illuminate\Cache\RedisStore' 586 | ); 587 | } 588 | 589 | try { 590 | 591 | $ids = $store->connection()->zrangebyscore( 592 | $this->cachePrefix.':index', 593 | $written_since, 594 | $written_until, 595 | [ 596 | 'LIMIT' => [ $skip, $take ], 597 | ] 598 | ); 599 | } 600 | catch ( \Exception $e ) { 601 | 602 | $ids = []; 603 | 604 | $this->handleCacheException( $e ); 605 | } 606 | 607 | if ( empty( $ids ) ) 608 | return new Collection; 609 | 610 | $keys = []; 611 | 612 | foreach ( $ids as $id ) { 613 | 614 | $keys[] = $this->cachePrefix.':'.$id; 615 | } 616 | 617 | unset( $ids ); 618 | 619 | try { 620 | 621 | $data = \call_user_func_array( 622 | [ $store, "mget" ], 623 | $keys 624 | ); 625 | } 626 | catch ( \Exception $e ) { 627 | 628 | $data = []; 629 | 630 | $this->handleCacheException( $e ); 631 | } 632 | 633 | unset( $keys ); 634 | 635 | return $this->unserializeMulti( $data ); 636 | } 637 | 638 | 639 | protected function getModelsFromBuffer ( 640 | int $written_since, 641 | int $written_until, 642 | int $skip, 643 | int $take 644 | ) : Collection 645 | { 646 | try { 647 | 648 | $store = Cache::getStore(); 649 | } 650 | catch ( \Exception $e ) { 651 | 652 | $this->handleCacheException( $e ); 653 | } 654 | 655 | if ( !$store instanceof RedisStore ) { 656 | 657 | throw new \Exception( 658 | 'persist() is only available for the following cache stores: Illuminate\Cache\RedisStore' 659 | ); 660 | } 661 | 662 | try { 663 | 664 | $result = $store->connection()->zrangebyscore( 665 | $this->cachePrefix.':buffer', 666 | $written_since, 667 | $written_until, 668 | [ 669 | 'LIMIT' => [ $skip, $take ], 670 | ] 671 | ); 672 | } 673 | catch ( \Exception $e ) { 674 | 675 | $result = []; 676 | 677 | $this->handleCacheException( $e ); 678 | } 679 | 680 | return $this->unserializeMulti( $result ); 681 | } 682 | 683 | 684 | protected function cleanIndex ( 685 | int $written_since, 686 | int $written_until, 687 | bool $forget = true 688 | ) 689 | { 690 | try { 691 | 692 | $store = Cache::getStore()->connection(); 693 | 694 | if ( $forget ) { 695 | 696 | \call_user_func_array( 697 | [ $store, "unlink" ], 698 | $keys 699 | ); 700 | } 701 | 702 | return $store->zremrangebyscore( 703 | $this->cachePrefix.':index', 704 | $written_since, 705 | $written_until 706 | ); 707 | } 708 | catch ( \Exception $e ) { 709 | 710 | $this->handleCacheException( $e ); 711 | } 712 | 713 | return false; 714 | } 715 | 716 | 717 | protected function cleanBuffer ( 718 | int $written_since, 719 | int $written_until 720 | ) 721 | { 722 | try { 723 | 724 | return Cache::getStore()->connection()->zremrangebyscore( 725 | $this->cachePrefix.':index', 726 | $written_since, 727 | $written_until 728 | ); 729 | } 730 | catch ( \Exception $e ) { 731 | 732 | $this->handleCacheException( $e ); 733 | } 734 | 735 | return false; 736 | } 737 | 738 | 739 | public function find ( 740 | Builder $queryBuilder = null 741 | ) : Collection 742 | { 743 | 744 | $class = $this->class; 745 | $ttl = $this->ttl; 746 | $hit = true; 747 | 748 | if ( $queryBuilder ) { 749 | 750 | $cacheKey = $this->generateQueryCacheKey( 751 | $queryBuilder 752 | ); 753 | } 754 | else { 755 | 756 | $cacheKey = $this->generateQueryCacheKey(); 757 | } 758 | 759 | $data = $this->query( 760 | $queryBuilder, 761 | function () use ( 762 | $class, 763 | $queryBuilder, 764 | $ttl, 765 | $hit 766 | ) 767 | { 768 | 769 | $hit = false; 770 | 771 | if ( $ttl == 0 ) { 772 | 773 | if ( $queryBuilder ) { 774 | 775 | return $queryBuilder->get(); 776 | } 777 | else { 778 | 779 | return $class::all(); 780 | } 781 | } 782 | 783 | if ( $queryBuilder ) { 784 | 785 | return $queryBuilder->get()->toArray(); 786 | } 787 | else { 788 | 789 | return $class::all()->toArray(); 790 | } 791 | }, 792 | $cacheKey 793 | ); 794 | 795 | 796 | $this->fireObserverEvent( 797 | 'afterFind', 798 | $hit, 799 | $data 800 | ); 801 | 802 | $this->clearSettings(); 803 | 804 | if ( $data instanceof Collection ) 805 | return $data; 806 | 807 | if ( !$data ) 808 | $data = []; 809 | 810 | $class::unguard(); 811 | 812 | $models = $class::hydrate( $data ); 813 | 814 | $class::reguard(); 815 | 816 | return $models; 817 | } 818 | 819 | 820 | public function count ( 821 | Builder $queryBuilder 822 | ) : int 823 | { 824 | $hit = true; 825 | 826 | $c = $this->query( 827 | $queryBuilder, 828 | function () use ( $queryBuilder, $hit ) { 829 | 830 | return $queryBuilder->get()->count(); 831 | }, 832 | $this->generateQueryCacheKey( 833 | $queryBuilder 834 | ).':count' 835 | ); 836 | 837 | $this->fireObserverEvent( 838 | 'afterCount', 839 | $hit, 840 | $c 841 | ); 842 | 843 | $this->clearSettings(); 844 | 845 | return $c; 846 | } 847 | 848 | 849 | public function generateQueryCacheKey ( 850 | Builder $queryBuilder = null 851 | ) : string 852 | { 853 | if ( $queryBuilder ) { 854 | 855 | return $this->cachePrefix.':q:'.$queryBuilder->getQuery()->generateCacheKey(); 856 | } 857 | else { 858 | 859 | return $this->cachePrefix.':q:all'; 860 | } 861 | } 862 | 863 | 864 | protected function storeModelInCache ( 865 | Model $model 866 | ) { 867 | 868 | return $this->writeToCache( 869 | 870 | $this->generateUnitCacheKey( 871 | $model 872 | ), 873 | $model->toArray() 874 | ); 875 | } 876 | 877 | 878 | public function generateUnitCacheKey ( 879 | Model $model 880 | ) : string 881 | { 882 | 883 | return $this->cachePrefix.':'.$model->getKey(); 884 | } 885 | 886 | 887 | public function detectCacheKey ( $value ) { 888 | 889 | if ( $value instanceof Model ) { 890 | 891 | return $this->cachePrefix.':'.$value->getKey(); 892 | } 893 | else if ( $value instanceof Builder ) { 894 | 895 | return $this->generateQueryCacheKey( $value ); 896 | } 897 | else if ( 898 | Str::startsWith( 899 | $value, 900 | $this->cachePrefix.':' 901 | ) 902 | ) { 903 | return $value; 904 | } 905 | else { 906 | 907 | return $this->cachePrefix.':'.$value; 908 | } 909 | } 910 | 911 | 912 | public function forget ( 913 | $id, 914 | array &$bulk = null 915 | ) 916 | { 917 | 918 | $this->clearSettings(); 919 | 920 | if ( isset($bulk) ) { 921 | 922 | $prevBulk = true; 923 | } 924 | else { 925 | 926 | $prevBulk = false; 927 | $bulk = []; 928 | } 929 | 930 | if ( 931 | \is_array( $id ) 932 | || $id instanceof Collection 933 | ) { 934 | 935 | foreach ( $id as $i ) { 936 | 937 | $key = $this->detectCacheKey( $i ); 938 | 939 | $bulk[] = $key; 940 | 941 | if ( $i instanceof Builder ) { 942 | 943 | $bulk[] = $key.':first'; 944 | $bulk[] = $key.':count'; 945 | } 946 | } 947 | } 948 | else { 949 | 950 | $key = $this->detectCacheKey( $id ); 951 | 952 | $bulk[] = $key; 953 | 954 | if ( $id instanceof Builder ) { 955 | 956 | $bulk[] = $key.':first'; 957 | $bulk[] = $key.':count'; 958 | } 959 | } 960 | 961 | if ( $prevBulk ) 962 | return $bulk; 963 | 964 | if ( empty($bulk) ) 965 | return false; 966 | 967 | try { 968 | 969 | $store = Cache::getStore(); 970 | } 971 | catch ( \Exception $e ) { 972 | 973 | $store = false; 974 | 975 | $this->handleCacheException( $e ); 976 | } 977 | 978 | if ( $store instanceof RedisStore ) { 979 | 980 | try { 981 | 982 | return \call_user_func_array( 983 | [ $store->connection(), "unlink" ], 984 | $bulk 985 | ); 986 | } 987 | catch ( \Exception $e ) { 988 | 989 | $this->handleCacheException( $e ); 990 | } 991 | } 992 | else { 993 | 994 | foreach ( $bulk as $key ) { 995 | 996 | try { 997 | 998 | Cache::forget( $key ); 999 | } 1000 | catch ( \Exception $e ) { 1001 | 1002 | $this->handleCacheException( $e ); 1003 | } 1004 | } 1005 | 1006 | return true; 1007 | } 1008 | 1009 | return false; 1010 | } 1011 | 1012 | 1013 | protected function getModelsToPersist ( 1014 | string $source, 1015 | int $written_since, 1016 | int $written_until, 1017 | int $skip, 1018 | int $take 1019 | ) { 1020 | 1021 | switch ( $source ) { 1022 | 1023 | case 'buffer': 1024 | return $this->getModelsFromBuffer( 1025 | $written_since, 1026 | $written_until, 1027 | $skip, 1028 | $take 1029 | ); 1030 | 1031 | case 'index': 1032 | return $this->getIndexedModels( 1033 | $written_since, 1034 | $written_until, 1035 | $skip, 1036 | $take 1037 | ); 1038 | 1039 | default: 1040 | throw new \Exception( 1041 | 'Unsupported option, value must be buffer or index' 1042 | ); 1043 | } 1044 | } 1045 | 1046 | 1047 | public function persist ( 1048 | \Closure $callback, 1049 | array $options = [] 1050 | ) : bool 1051 | { 1052 | $written_since = $options['written_since'] ?? 0; 1053 | $written_until = $options['written_until'] ?? \time(); 1054 | $object_limit = $options['object_limit'] ?? self::OBJECT_LIMIT; 1055 | 1056 | $source = $options['method'] ?? 'buffer'; 1057 | 1058 | $skip = 0; 1059 | $take = $object_limit; 1060 | 1061 | $models = $this->getModelsToPersist( 1062 | $source, 1063 | $written_since, 1064 | $written_until, 1065 | $skip, 1066 | $take 1067 | ); 1068 | 1069 | $r = true; 1070 | 1071 | while ( !$models->isEmpty() ) { 1072 | 1073 | if ( !$callback( $models ) ) { 1074 | 1075 | $r = false; 1076 | break; 1077 | } 1078 | 1079 | $skip += $object_limit; 1080 | $take += $object_limit; 1081 | 1082 | $models = $this->getModelsToPersist( 1083 | $source, 1084 | $written_since, 1085 | $written_until, 1086 | $skip, 1087 | $take 1088 | ); 1089 | } 1090 | 1091 | if ( 1092 | $r 1093 | && $options['clean_cache'] ?? true 1094 | ) { 1095 | 1096 | $this->cleanAfterPersist( 1097 | $source, 1098 | $written_since, 1099 | $written_until 1100 | ); 1101 | } 1102 | 1103 | return $r; 1104 | } 1105 | 1106 | 1107 | protected function query ( 1108 | Builder $queryBuilder = null, 1109 | \Closure $callback, 1110 | string $key 1111 | ) 1112 | { 1113 | $class = $this->class; 1114 | 1115 | if ( $this->skip && $queryBuilder ) { 1116 | 1117 | $queryBuilder->skip( $this->skip ); 1118 | } 1119 | 1120 | if ( $this->take && $queryBuilder ) { 1121 | 1122 | $queryBuilder->take( $this->take ); 1123 | } 1124 | 1125 | if ( $this->fromCache ) { 1126 | 1127 | try { 1128 | 1129 | return Cache::get( $key ); 1130 | } 1131 | catch ( \Exception $e ) { 1132 | 1133 | return null; 1134 | 1135 | $this->handleCacheException( $e ); 1136 | } 1137 | } 1138 | else if ( $this->ttl < 0 ) { 1139 | 1140 | try { 1141 | 1142 | return Cache::rememberForever( 1143 | $key, 1144 | $callback 1145 | ); 1146 | } 1147 | catch ( \Exception $e ) { 1148 | 1149 | return null; 1150 | 1151 | $this->handleCacheException( $e ); 1152 | } 1153 | } 1154 | else if ( $this->ttl > 0 ) { 1155 | 1156 | try { 1157 | 1158 | $r = Cache::remember( 1159 | $key, 1160 | $this->ttl, 1161 | $callback 1162 | ); 1163 | } 1164 | catch ( \Exception $e ) { 1165 | 1166 | $r = null; 1167 | $this->handleCacheException( $e ); 1168 | } 1169 | 1170 | return $r; 1171 | } 1172 | else { 1173 | 1174 | return $callback(); 1175 | } 1176 | } 1177 | 1178 | 1179 | protected function writeToCache ( 1180 | string $key, 1181 | $value 1182 | ) 1183 | { 1184 | if ( $this->ttl < 0 ) { 1185 | 1186 | try { 1187 | 1188 | return Cache::forever( 1189 | $key, 1190 | $value 1191 | ); 1192 | } 1193 | catch ( \Exception $e ) { 1194 | 1195 | $this->handleCacheException( $e ); 1196 | } 1197 | } 1198 | else if ( $this->ttl > 0 ) { 1199 | 1200 | try { 1201 | 1202 | return Cache::put( 1203 | $key, 1204 | $value, 1205 | $this->ttl 1206 | ); 1207 | } 1208 | catch ( \Exception $e ) { 1209 | 1210 | $this->handleCacheException( $e ); 1211 | } 1212 | } 1213 | 1214 | return false; 1215 | } 1216 | 1217 | 1218 | protected function cleanAfterPersist ( 1219 | string $source, 1220 | int $written_since, 1221 | int $written_until 1222 | ) { 1223 | 1224 | switch ( $source ) { 1225 | 1226 | case 'buffer': 1227 | return $this->cleanBuffer( 1228 | $written_since, 1229 | $written_until 1230 | ); 1231 | 1232 | case 'index': 1233 | return $this->cleanIndex( 1234 | $written_since, 1235 | $written_until, 1236 | ); 1237 | 1238 | default: 1239 | throw new \Exception( 1240 | 'Unsupported option, value must be buffer or index' 1241 | ); 1242 | } 1243 | } 1244 | 1245 | 1246 | protected function handleDbException ( \Exception $e ) { 1247 | 1248 | if ( $this->dbHandler ) { 1249 | 1250 | $callback = $this->dbHandler; 1251 | 1252 | return $callback( $e ); 1253 | } 1254 | 1255 | if ( !$this->mute ) 1256 | throw $e; 1257 | } 1258 | 1259 | 1260 | protected function handleCacheException ( \Exception $e ) { 1261 | 1262 | if ( $this->cacheHandler ) { 1263 | 1264 | $callback = $this->cacheHandler; 1265 | 1266 | return $callback( $e ); 1267 | } 1268 | 1269 | if ( !$this->mute ) 1270 | throw $e; 1271 | } 1272 | 1273 | 1274 | public function handleDatabaseExceptions ( 1275 | \Closure $callback 1276 | ){ 1277 | $this->dbHandler = $callback; 1278 | } 1279 | 1280 | 1281 | public function handleCacheStoreExceptions ( 1282 | \Closure $callback 1283 | ){ 1284 | $this->cacheHandler = $callback; 1285 | } 1286 | 1287 | } -------------------------------------------------------------------------------- /src/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | remember() then stores model in cache and returns true, otherwise returns $this and must be followed by one of the following methods get(), find(), create(), count(), first(), delete(), skip(), take() or save() 23 | public function during ( int $seconds ); 24 | 25 | 26 | // Does almost the same as during() but it reads the amount of seconds from a given model's attribute 27 | public function according ( string $attribute ); 28 | 29 | 30 | // remembers object or queries in cache forever. 31 | // If model is passed returns boolean, otherwise returns $this 32 | // Write-Aside Strategy 33 | public function rememberForever ( 34 | Model $model = null 35 | ); 36 | 37 | 38 | // retrieves object or queries directly and only from cache. 39 | // Force Read-Aside Strategy 40 | public function fromCache ( ); // return $this; 41 | 42 | 43 | // retrieves a specific object. 44 | // Read-Through Strategy 45 | public function get ( $id ) : ?Model; 46 | 47 | // inserts a specific object. 48 | // Write-Through Strategy 49 | public function create ( array $attributes ) : ?Model; 50 | 51 | // inserts or updates a specific object. 52 | // Write-Through Strategy 53 | public function save ( Model $model ) : bool; 54 | 55 | // deletes a specific object. 56 | // Write-Through Strategy 57 | public function delete ( Model $model ) : bool; 58 | 59 | // returns first matched object. 60 | // Read-Through Strategy 61 | public function first ( Builder $queryBuilder ) : ?Model; 62 | 63 | // returns all matched objects. 64 | // Read-Through Strategy 65 | public function find ( Builder $queryBuilder ) : Collection; 66 | 67 | // returns the number of matched object. 68 | // Read-Through Strategy 69 | public function count ( Builder $queryBuilder ) : int; 70 | 71 | 72 | // sets the amount of entities to be retrieved using find() method 73 | public function take ( int $entities ); // return $this; 74 | 75 | 76 | // sets the amount of entities to be skiped from the begining of the result 77 | public function skip ( int $entities ); // return $this; 78 | 79 | 80 | // removes a specific object from cache only 81 | public function forget ( 82 | $id, 83 | array &$bulk = null 84 | ); 85 | 86 | 87 | // saves complete model's data in a buffer in order to be processed by persist later 88 | public function buffer ( Model $model ); 89 | 90 | 91 | // saves model's id in an index to be processed by persist later. NOTE: during persist model data will be retrieved from the model's specific cache key 92 | public function index ( Model $model ); 93 | 94 | 95 | // runs a callback for all models stored in cache using ->rememberForever( $model ) method allowing to persist them in database or any other processing (Write-Back Strategy) 96 | // options: 97 | /* 98 | [ 99 | 'written_since' => 0, 100 | 'written_until' => \time(), 101 | 'object_limit' => 1000, 102 | 'clean_cache' => true, 103 | 'source' => 'buffer' | 'index', 104 | ] 105 | */ 106 | public function persist ( 107 | \Closure $callback, 108 | array $options = [] 109 | ) : bool; 110 | 111 | 112 | public function handleDatabaseExceptions ( 113 | \Closure $callback 114 | ); 115 | 116 | 117 | public function handleCacheStoreExceptions ( 118 | \Closure $callback 119 | ); 120 | 121 | 122 | public function observe ( \Closure $callback ); 123 | 124 | public function observeAlways ( 125 | string $event, 126 | \Closure $callback 127 | ); 128 | 129 | 130 | public function silently ( ); 131 | 132 | } -------------------------------------------------------------------------------- /write-back-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krazydanny/laravel-repository/835d10a83301e1bebc0b3be51c80b2808303570e/write-back-cache.png -------------------------------------------------------------------------------- /write-through-cache.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krazydanny/laravel-repository/835d10a83301e1bebc0b3be51c80b2808303570e/write-through-cache.png --------------------------------------------------------------------------------