├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── config └── config.php ├── phpstan.neon.dist └── src ├── Contracts ├── CacheableContract.php ├── CriterionContract.php └── RepositoryContract.php ├── Exceptions ├── CriterionException.php ├── EntityNotFoundException.php └── RepositoryException.php ├── Listeners └── RepositoryEventListener.php ├── Providers └── RepositoryServiceProvider.php ├── Repositories ├── BaseRepository.php └── EloquentRepository.php └── Traits ├── Bindable.php ├── Cacheable.php └── Criteriable.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Rinvex Repository Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | This project adheres to [Semantic Versioning](CONTRIBUTING.md). 6 | 7 | 8 | ## [v6.0.3] - 2020-04-09 9 | - Reverse commit "Convert database int fields into bigInteger" 10 | 11 | ## [v6.0.2] - 2020-04-04 12 | - Fix namespace issue 13 | 14 | ## [v6.0.1] - 2020-04-04 15 | - Enforce consistent package namespace 16 | - Drop laravel/helpers usage as it's no longer used 17 | 18 | ## [v6.0.0] - 2020-03-15 19 | - Upgrade to Laravel v7.1.x & PHP v7.4.x 20 | 21 | ## [v5.0.2] - 2020-03-13 22 | - Tweak TravisCI config 23 | - Drop using global helpers 24 | - Update StyleCI config 25 | 26 | ## [v5.0.1] - 2019-09-24 27 | - Add missing laravel/helpers composer package 28 | 29 | ## [v5.0.0] - 2019-09-23 30 | - Upgrade to Laravel v6 and update dependencies 31 | 32 | ## [v4.1.0] - 2019-06-02 33 | - Update composer deps 34 | - Drop PHP 7.1 travis test 35 | - Fix deprecated fire => dispatch method 36 | 37 | ## [v4.0.0] - 2019-03-03 38 | - Rename environment variable QUEUE_DRIVER to QUEUE_CONNECTION 39 | - Require PHP 7.2 & Laravel 5.8 40 | - Apply PHPUnit 8 updates 41 | - Replace __ CLASS __ & get_called_class() with self::class & static::class (potentially deprecated in PHP 7.4) 42 | - Laravel 5.8 changed cache TTL to use seconds not minutes 43 | 44 | ## [v3.0.1] - 2018-12-22 45 | - Add Laravel v5.7 support 46 | - Update composer dependencies 47 | - Add PHP 7.3 support to travis 48 | 49 | ## [v3.0.0] - 2018-10-01 50 | - Enforce Consistency 51 | - Support Laravel 5.7+ 52 | - Rename package to rinvex/laravel-repositories 53 | 54 | ## [v2.0.3] - 2017-01-27 55 | - Revert "Add support for Laravel 5.4" (v2.x won't get Laravel 5.4 support #131) 56 | 57 | ## [v2.0.2] - 2017-01-27 58 | - Add support for Laravel 5.4 59 | 60 | ## [v2.0.1] - 2016-08-06 61 | - Add missing argument to simplePaginate method (#54) 62 | - Add config option for default model directory (close #55) 63 | - Extract Cache methods to a Contract / Trait (close #57) 64 | - Fix spelling typos & fix docs 65 | - Added extra logic to paginate and simplePaginate methods, fixes #61 (#62) 66 | 67 | ## [v2.0.0] - 2016-07-01 68 | - Drop `findOrCreate` method (close #33) 69 | - Change `retrieveModel` behavior (close #34) 70 | - Separate functionality into `setModel`, `getModel`, and `createModel` 71 | - `createModel` always return a new clean model instance 72 | - Drop `enableCache` & `isCacheEnabled` (close #35) 73 | - These methods have duplicate functionality of `setCacheLifetime` & `getCacheLifetime` 74 | - Add filtration through `where`, 'whereIn', `whereNotIn` methods 75 | - Add `offset`, and `limit` functionality to the query 76 | - Drop `addGlobalScope`, `withoutGlobalScopes` methods (close #8) 77 | - Move `$with` argument to setters & getters (close #36) 78 | - Rename `$column` to `$attribute` for more naming abstraction (close #37) 79 | - Add `where`, `whereIn`, `whereNotIn` methods for flexible filtration (close #6) 80 | - Update `findWhere`, `findWhereIn`, `findWhereNotIn` methods to utilize the new filtration (close #38) 81 | - Centralize & enforce filtration and data access rules (close #39) 82 | - Add `offset` & `limit` functionality to queries (close #40) 83 | - Add `simplePaginate` method for light weight pagination (close #41) 84 | - Refactor callback execution, clean code, and reset query conditions after execution (close #30, close #42) 85 | 86 | ## [v1.0.5] - 2016-06-27 87 | - Fix clear cache on update issue (close #27) 88 | - Move cache lifetime & driver args to setter methods (close #26) 89 | - Review & rewrite documentation to reflect recent updates 90 | 91 | ## [v1.0.4] - 2016-06-24 92 | - Add Laravel 5.3.* support 93 | - Update chat link 94 | - Review and enhance documentation from scratch 95 | - Add gif for quick example workflow 96 | - Add visual graphics for better attraction & understanding 97 | - Enhance the whole documentation framework and outlines 98 | - Add quick example section & isolate the advanced details 99 | - Add example for Coding To An Interface 100 | - Fix fired event names typo (close #25) 101 | - Update contributing guidelines 102 | 103 | ## [v1.0.3] - 2016-06-22 104 | - Fix wrong RepositoryServiceProvider PSR-4 namespace (close #19) 105 | 106 | ## [v1.0.2] - 2016-06-22 107 | - Fix `findWhere` wrong results and fix docs mistakes (close #15) 108 | - Enable/disable cache per query (close #16) 109 | - Revamp the entire documentation (close #17) 110 | 111 | ## [v1.0.1] - 2016-06-21 112 | - Update docs, docblocks, and fix homepage link 113 | - Add per repository cache lifetime/driver support (Close #10) 114 | 115 | ## v1.0.0 - 2016-06-18 116 | - Tag first release 117 | 118 | [v6.0.3]: https://github.com/rinvex/laravel-repositories/compare/v6.0.2...v6.0.3 119 | [v6.0.2]: https://github.com/rinvex/laravel-repositories/compare/v6.0.1...v6.0.2 120 | [v6.0.1]: https://github.com/rinvex/laravel-repositories/compare/v6.0.0...v6.0.1 121 | [v6.0.0]: https://github.com/rinvex/laravel-repositories/compare/v5.0.2...v6.0.0 122 | [v5.0.2]: https://github.com/rinvex/laravel-repositories/compare/v5.0.1...v5.0.2 123 | [v5.0.1]: https://github.com/rinvex/laravel-repositories/compare/v5.0.0...v5.0.1 124 | [v5.0.0]: https://github.com/rinvex/laravel-repositories/compare/v4.1.0...v5.0.0 125 | [v4.1.0]: https://github.com/rinvex/laravel-repositories/compare/v4.0.0...v4.1.0 126 | [v4.0.0]: https://github.com/rinvex/laravel-repositories/compare/v3.0.1...v4.0.0 127 | [v3.0.1]: https://github.com/rinvex/laravel-repositories/compare/v3.0.0...v3.0.1 128 | [v3.0.0]: https://github.com/rinvex/laravel-repositories/compare/v2.0.3...v3.0.0 129 | [v2.0.3]: https://github.com/rinvex/laravel-repositories/compare/v2.0.2...v2.0.3 130 | [v2.0.2]: https://github.com/rinvex/laravel-repositories/compare/v2.0.1...v2.0.2 131 | [v2.0.1]: https://github.com/rinvex/laravel-repositories/compare/v2.0.0...v2.0.1 132 | [v2.0.0]: https://github.com/rinvex/laravel-repositories/compare/v1.0.5...v2.0.0 133 | [v1.0.5]: https://github.com/rinvex/laravel-repositories/compare/v1.0.4...v1.0.5 134 | [v1.0.4]: https://github.com/rinvex/laravel-repositories/compare/v1.0.3...v1.0.4 135 | [v1.0.3]: https://github.com/rinvex/laravel-repositories/compare/v1.0.2...v1.0.3 136 | [v1.0.2]: https://github.com/rinvex/laravel-repositories/compare/v1.0.1...v1.0.2 137 | [v1.0.1]: https://github.com/rinvex/laravel-repositories/compare/v1.0.0...v1.0.1 138 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | nationality, personal appearance, race, religion, or sexual identity and 10 | orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [help@rinvex.com](mailto:help@rinvex.com). All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at [http://contributor-covenant.org/version/1/4][version] 72 | 73 | [homepage]: http://contributor-covenant.org 74 | [version]: http://contributor-covenant.org/version/1/4/ 75 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guide 2 | 3 | This project adheres to the following standards and practices. 4 | 5 | 6 | ## Versioning 7 | 8 | This project is versioned under the [Semantic Versioning](http://semver.org/) guidelines as much as possible. 9 | 10 | Releases will be numbered with the following format: 11 | 12 | - `..` 13 | - `..` 14 | 15 | And constructed with the following guidelines: 16 | 17 | - Breaking backward compatibility bumps the major and resets the minor and patch. 18 | - New additions without breaking backward compatibility bump the minor and reset the patch. 19 | - Bug fixes and misc changes bump the patch. 20 | 21 | 22 | ## Pull Requests 23 | 24 | The pull request process differs for new features and bugs. 25 | 26 | Pull requests for bugs may be sent without creating any proposal issue. If you believe that you know of a solution for a bug that has been filed, please leave a comment detailing your proposed fix or create a pull request with the fix mentioning that issue id. 27 | 28 | 29 | ## Coding Standards 30 | 31 | This project follows the FIG PHP Standards Recommendations compliant with the [PSR-1: Basic Coding Standard](http://www.php-fig.org/psr/psr-1/), [PSR-2: Coding Style Guide](http://www.php-fig.org/psr/psr-2/) and [PSR-4: Autoloader](http://www.php-fig.org/psr/psr-4/) to ensure a high level of interoperability between shared PHP code. If you notice any compliance oversights, please send a patch via pull request. 32 | 33 | 34 | ## Feature Requests 35 | 36 | If you have a proposal or a feature request, you may create an issue with `[Proposal]` in the title. 37 | 38 | The proposal should also describe the new feature, as well as implementation ideas. The proposal will then be reviewed and either approved or denied. Once a proposal is approved, a pull request may be created implementing the new feature. 39 | 40 | 41 | ## Git Flow 42 | 43 | This project follows [Git-Flow](http://nvie.com/posts/a-successful-git-branching-model/), and as such has `master` (latest stable releases), `develop` (latest WIP development) and X.Y support branches (when there's multiple major versions). 44 | 45 | Accordingly all pull requests MUST be sent to the `develop` branch. 46 | 47 | > **Note:** Pull requests which do not follow these guidelines will be closed without any further notice. 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020, Rinvex LLC, 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 | # Rinvex Repository 2 | 3 | ⚠️ **This package is abandoned and no longer maintained. No replacement package was suggested.** ⚠️ 4 | 5 | 👉 [Contact me if you are interested in maintaining it!](https://twitter.com/Omranic) 6 | 7 | 8 | ![Rinvex Repository Diagram](https://rinvex.com/assets/frontend/layout/img/products/rinvex.repository.v2.diagram.png) 9 | 10 | **Rinvex Repository** is a simple, intuitive, and smart implementation of Active Repository with extremely flexible & granular caching system for Laravel, used to abstract the data layer, making applications more flexible to maintain. 11 | 12 | [![Packagist](https://img.shields.io/packagist/v/rinvex/laravel-repositories.svg?label=Packagist&style=flat-square)](https://packagist.org/packages/rinvex/laravel-repositories) 13 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/rinvex/laravel-repositories.svg?label=Scrutinizer&style=flat-square)](https://scrutinizer-ci.com/g/rinvex/laravel-repositories/) 14 | [![Travis](https://img.shields.io/travis/rinvex/laravel-repositories.svg?label=TravisCI&style=flat-square)](https://travis-ci.org/rinvex/laravel-repositories) 15 | [![StyleCI](https://styleci.io/repos/61269204/shield)](https://styleci.io/repos/61269204) 16 | [![License](https://img.shields.io/packagist/l/rinvex/laravel-repositories.svg?label=License&style=flat-square)](https://github.com/rinvex/laravel-repositories/blob/develop/LICENSE) 17 | 18 | 19 | 💡 If you are looking for **Laravel 5.5** support, use the `dev-develop` branch. It's stable but not tagged yet since test suites isn't complete. 💡 20 | 21 | 22 | ## Features 23 | 24 | - Cache, Cache, Cache! 25 | - Prevent code duplication. 26 | - Reduce potential programming errors. 27 | - Granularly cache queries with flexible control. 28 | - Apply centrally managed, consistent access rules and logic. 29 | - Implement and centralize a caching strategy for the domain model. 30 | - Improve the code’s maintainability and readability by separating client objects from domain models. 31 | - Maximize the amount of code that can be tested with automation and to isolate both the client object and the domain model to support unit testing. 32 | - Associate a behavior with the related data. For example, calculate fields or enforce complex relationships or business rules between the data elements within an entity. 33 | 34 | 35 | ## Quick Example (TL;DR) 36 | 37 | The `Rinvex\Repository\Repositories\BaseRepository` is an abstract class with bare minimum that concrete implementations must extend. 38 | 39 | The `Rinvex\Repository\Repositories\EloquentRepository` is currently the only available repository implementation (more to come in the future and [you can develop your own](#add-custom-implementation)), it makes it easy to create new eloquent model instances and to manipulate them easily. To use `EloquentRepository` your repository MUST extend it first: 40 | 41 | ```php 42 | namespace App\Repositories; 43 | 44 | use Rinvex\Repository\Repositories\EloquentRepository; 45 | 46 | class FooRepository extends EloquentRepository 47 | { 48 | protected $repositoryId = 'rinvex.repository.uniqueid'; 49 | 50 | protected $model = 'App\Models\User'; 51 | } 52 | ``` 53 | That's it, you're done! Yes, it's that simple. 54 | 55 | But if you'd like more control over the container instance, or would like to pass model name dynamically you can alternatively do as follow: 56 | 57 | ```php 58 | namespace App\Repositories; 59 | 60 | use Illuminate\Contracts\Container\Container; 61 | use Rinvex\Repository\Repositories\EloquentRepository; 62 | 63 | class FooRepository extends EloquentRepository 64 | { 65 | // Instantiate repository object with required data 66 | public function __construct(Container $container) 67 | { 68 | $this->setContainer($container) 69 | ->setModel(\App\Models\User::class) 70 | ->setRepositoryId('rinvex.repository.uniqueid'); 71 | 72 | } 73 | } 74 | ``` 75 | 76 | Now inside your controller, you can either instantiate the repository traditionally through `$repository = new \App\Repositories\FooRepository();` or to use Laravel's awesome dependency injection and let the IoC do the magic: 77 | 78 | ```php 79 | namespace App\Http\Controllers; 80 | 81 | use App\Repositories\FooRepository; 82 | 83 | class BarController 84 | { 85 | // Inject `FooRepository` from the IoC 86 | public function baz(FooRepository $repository) 87 | { 88 | // Find entity by primary key 89 | $repository->find(1); 90 | 91 | // Find all entities 92 | $repository->findAll(); 93 | 94 | // Create a new entity 95 | $repository->create(['name' => 'Example']); 96 | } 97 | } 98 | ``` 99 | 100 | **Rinvex Repository Workflow - Create Repository** 101 | ![Rinvex Repository Workflow - Create Repository](https://rinvex.com/assets/frontend/layout/img/products/rinvex.repository.v2.workflow-1.gif) 102 | 103 | **Rinvex Repository Workflow - Use In Controller** 104 | ![Rinvex Repository Workflow - Use In Controller](https://rinvex.com/assets/frontend/layout/img/products/rinvex.repository.v2.workflow-2.gif) 105 | 106 | [UML Diagram](https://rinvex.com/assets/frontend/layout/img/products/rinvex.repository.v2.uml-diagram.png) 107 | 108 | --- 109 | 110 | **Mission accomplished! You're good to use this package right now! :white_check_mark:** 111 | 112 | **Unless you need to dig deeper & know some advanced stuff, you can skip the following steps! :wink:** 113 | 114 | --- 115 | 116 | 117 | ## Table Of Contents 118 | 119 | - [Installation](#installation) 120 | - [Compatibility](#compatibility) 121 | - [Require Package](#require-package) 122 | - [Install Dependencies](#install-dependencies) 123 | - [Integration](#integration) 124 | - [Native Integration](#native-integration) 125 | - [Laravel Integration](#laravel-integration) 126 | - [Configuration](#configuration) 127 | - [Usage](#usage) 128 | - [Quick Example](#quick-example) 129 | - [Detailed Documentation](#detailed-documentation) 130 | - [`setContainer()`, `getContainer()`](#setcontainer-getcontainer) 131 | - [`setConnection()`, `getConnection()`](#setconnection-getconnection) 132 | - [`setModel()`, `getModel()`](#setmodel-getmodel) 133 | - [`setRepositoryId()`, `getRepositoryId()`](#setrepositoryid-getrepositoryid) 134 | - [`setCacheLifetime()`, `getCacheLifetime()`](#setcachelifetime-getcachelifetime) 135 | - [`setCacheDriver()`, `getCacheDriver()`](#setcachedriver-getcachedriver) 136 | - [`enableCacheClear()`, `isCacheClearEnabled()`](#enablecacheclear-iscacheclearenabled) 137 | - [`createModel()`](#createmodel) 138 | - [`forgetCache()`](#forgetcache) 139 | - [`with()`](#with) 140 | - [`where()`](#where) 141 | - [`whereIn()`](#wherein) 142 | - [`whereNotIn()`](#wherenotin) 143 | - [`whereHas()`](#wherehas) 144 | - [`offset()`](#offset) 145 | - [`limit()`](#limit) 146 | - [`orderBy()`](#orderby) 147 | - [`find()`](#find) 148 | - [`findBy()`](#findby) 149 | - [`findFirst()`](#findFirst) 150 | - [`findAll()`](#findall) 151 | - [`paginate()`](#paginate) 152 | - [`simplePaginate()`](#simplepaginate) 153 | - [`findWhere()`](#findwhere) 154 | - [`findWhereIn()`](#findwherein) 155 | - [`findWhereNotIn()`](#findwherenotin) 156 | - [`findWhereHas()`](#findwherehas) 157 | - [`create()`](#create) 158 | - [`update()`](#update) 159 | - [`store()`](#store) 160 | - [`delete()`](#delete) 161 | - [`beginTransaction()`](#begintransaction) 162 | - [`commit()`](#commit) 163 | - [`rollBack()`](#rollback) 164 | - [Code To An Interface](#code-to-an-interface) 165 | - [Add Custom Implementation](#add-custom-implementation) 166 | - [EloquentRepository Fired Events](#eloquentrepository-fired-events) 167 | - [Mandatory Repository Conventions](#mandatory-repository-conventions) 168 | - [Automatic Guessing](#automatic-guessing) 169 | - [Flexible & Granular Caching](#flexible--granular-caching) 170 | - [Whole Application Cache](#whole-application-cache) 171 | - [Individual Query Cache](#individual-query-cache) 172 | - [Temporary Skip Individual HTTP Request Cache](#temporary-skip-individual-http-request-cache) 173 | - [Final Thoughts](#final-thoughts) 174 | - [Changelog](#changelog) 175 | - [Support](#support) 176 | - [Contributing & Protocols](#contributing--protocols) 177 | - [Security Vulnerabilities](#security-vulnerabilities) 178 | - [About Rinvex](#about-rinvex) 179 | - [License](#license) 180 | 181 | 182 | ## Installation 183 | 184 | The best and easiest way to install this package is through [Composer](https://getcomposer.org/). 185 | 186 | ### Compatibility 187 | 188 | This package fully compatible with **Laravel** `5.1.*`, `5.2.*`, and `5.3.*`. 189 | 190 | While this package tends to be framework-agnostic, it embraces Laravel culture and best practices to some extent. It's tested mainly with Laravel but you still can use it with other frameworks or even without any framework if you want. 191 | 192 | ### Require Package 193 | 194 | Open your application's `composer.json` file and add the following line to the `require` array: 195 | ```json 196 | "rinvex/laravel-repositories": "3.0.*" 197 | ``` 198 | 199 | > **Note:** Make sure that after the required changes your `composer.json` file is valid by running `composer validate`. 200 | 201 | ### Install Dependencies 202 | 203 | On your terminal run `composer install` or `composer update` command according to your application's status to install the new requirements. 204 | 205 | > **Note:** Checkout Composer's [Basic Usage](https://getcomposer.org/doc/01-basic-usage.md) documentation for further details. 206 | 207 | 208 | ## Integration 209 | 210 | **Rinvex Repository** package is framework-agnostic and as such can be integrated easily natively or with your favorite framework. 211 | 212 | ### Native Integration 213 | 214 | Integrating the package outside of a framework is incredibly easy, just require the `vendor/autoload.php` file to autoload the package. 215 | 216 | > **Note:** Checkout Composer's [Autoloading](https://getcomposer.org/doc/01-basic-usage.md#autoloading) documentation for further details. 217 | 218 | Run the following command on your terminal to publish config files: 219 | ```shell 220 | php artisan vendor:publish --tag="rinvex-repository-config" 221 | ``` 222 | 223 | > **Note:** Checkout Laravel's [Configuration](https://laravel.com/docs/master/#configuration) documentation for further details. 224 | 225 | You are good to go. Integration is done and you can now use all the available methods, proceed to the [Usage](#usage) section for an example. 226 | 227 | 228 | ## Configuration 229 | 230 | If you followed the previous integration steps, then your published config file reside at `config/rinvex.repository.php`. 231 | 232 | Config options are very expressive and self explanatory, as follows: 233 | 234 | ```php 235 | return [ 236 | 237 | /* 238 | |-------------------------------------------------------------------------- 239 | | Models Directory 240 | |-------------------------------------------------------------------------- 241 | | 242 | | Here you may specify the default models directory, just write 243 | | directory name, like 'Models' not the full path. 244 | | 245 | | Default: 'Models' 246 | | 247 | */ 248 | 249 | 'models' => 'Models', 250 | 251 | /* 252 | |-------------------------------------------------------------------------- 253 | | Caching Strategy 254 | |-------------------------------------------------------------------------- 255 | */ 256 | 257 | 'cache' => [ 258 | 259 | /* 260 | |-------------------------------------------------------------------------- 261 | | Cache Keys File 262 | |-------------------------------------------------------------------------- 263 | | 264 | | Here you may specify the cache keys file that is used only with cache 265 | | drivers that does not support cache tags. It is mandatory to keep 266 | | track of cache keys for later usage on cache flush process. 267 | | 268 | | Default: storage_path('framework/cache/rinvex.repository.json') 269 | | 270 | */ 271 | 272 | 'keys_file' => storage_path('framework/cache/rinvex.repository.json'), 273 | 274 | /* 275 | |-------------------------------------------------------------------------- 276 | | Cache Lifetime 277 | |-------------------------------------------------------------------------- 278 | | 279 | | Here you may specify the number of seconds that you wish the cache 280 | | to be remembered before it expires. If you want the cache to be 281 | | remembered forever, set this option to -1. 0 means disabled. 282 | | 283 | | Default: -1 284 | | 285 | */ 286 | 287 | 'lifetime' => -1, 288 | 289 | /* 290 | |-------------------------------------------------------------------------- 291 | | Cache Clear 292 | |-------------------------------------------------------------------------- 293 | | 294 | | Specify which actions would you like to clear cache upon success. 295 | | All repository cached data will be cleared accordingly. 296 | | 297 | | Default: ['create', 'update', 'delete'] 298 | | 299 | */ 300 | 301 | 'clear_on' => [ 302 | 'create', 303 | 'update', 304 | 'delete', 305 | ], 306 | 307 | /* 308 | |-------------------------------------------------------------------------- 309 | | Cache Skipping URI 310 | |-------------------------------------------------------------------------- 311 | | 312 | | For testing purposes, or maybe some certain situations, you may wish 313 | | to skip caching layer and get fresh data result set just for the 314 | | current request. This option allows you to specify custom 315 | | URL parameter for skipping caching layer easily. 316 | | 317 | | Default: 'skipCache' 318 | | 319 | */ 320 | 321 | 'skip_uri' => 'skipCache', 322 | 323 | ], 324 | 325 | ]; 326 | ``` 327 | 328 | 329 | ## Usage 330 | 331 | ### Detailed Documentation 332 | 333 | #### `setContainer()`, `getContainer()` 334 | 335 | The `setContainer` method sets the IoC container instance, while `getContainer` returns it: 336 | 337 | ```php 338 | // Set the IoC container instance 339 | $repository->setContainer(new \Illuminate\Container\Container()); 340 | 341 | // Get the IoC container instance 342 | $container = $repository->getContainer(); 343 | ``` 344 | 345 | #### `setConnection()`, `getConnection()` 346 | 347 | The `setConnection` method sets the connection associated with the repository, while `getConnection` returns it: 348 | 349 | ```php 350 | // Set the connection associated with the repository 351 | $repository->setConnection('mysql'); 352 | 353 | // Get the current connection for the repository 354 | $connection = $repository->getConnection(); 355 | ``` 356 | 357 | > **Note:** The name passed to the `setConnection` method should correspond to one of the connections listed in your `config/database.php` configuration file. 358 | 359 | #### `setModel()`, `getModel()` 360 | 361 | The `setModel` method sets the repository model, while `getModel` returns it: 362 | 363 | ```php 364 | // Set the repository model 365 | $repository->setModel(\App\Models\User::class); 366 | 367 | // Get the repository model 368 | $repositoryModel = $repository->getModel(); 369 | ``` 370 | 371 | #### `setRepositoryId()`, `getRepositoryId()` 372 | 373 | The `setRepositoryId` method sets the repository identifier, while `getRepositoryId` returns it (it could be anything you want, but must be **unique per repository**): 374 | 375 | ```php 376 | // Set the repository identifier 377 | $repository->setRepositoryId('rinvex.repository.uniqueid'); 378 | 379 | // Get the repository identifier 380 | $repositoryId = $repository->getRepositoryId(); 381 | ``` 382 | 383 | #### `setCacheLifetime()`, `getCacheLifetime()` 384 | 385 | The `setCacheLifetime` method sets the repository cache lifetime, while `getCacheLifetime` returns it: 386 | 387 | ```php 388 | // Set the repository cache lifetime 389 | $repository->setCacheLifetime(123); 390 | 391 | // Get the repository cache lifetime 392 | $cacheLifetime = $repository->getCacheLifetime(); 393 | ``` 394 | 395 | #### `setCacheDriver()`, `getCacheDriver()` 396 | 397 | The `setCacheDriver` method sets the repository cache driver, while `getCacheDriver` returns it: 398 | 399 | ```php 400 | // Set the repository cache driver 401 | $repository->setCacheDriver('redis'); 402 | 403 | // Get the repository cache driver 404 | $cacheDriver = $repository->getCacheDriver(); 405 | ``` 406 | 407 | #### `enableCacheClear()`, `isCacheClearEnabled()` 408 | 409 | The `enableCacheClear` method enables repository cache clear, while `isCacheClearEnabled` determines it's state: 410 | 411 | ```php 412 | // Enable repository cache clear 413 | $repository->enableCacheClear(true); 414 | 415 | // Disable repository cache clear 416 | $repository->enableCacheClear(false); 417 | 418 | // Determine if repository cache clear is enabled 419 | $cacheClearStatus = $repository->isCacheClearEnabled(); 420 | ``` 421 | 422 | #### `createModel()` 423 | 424 | The `createModel()` method creates a new repository model instance: 425 | 426 | ```php 427 | $repositoryModelInstance = $repository->createModel(); 428 | ``` 429 | 430 | #### `forgetCache()` 431 | 432 | The `forgetCache()` method forgets the repository cache: 433 | 434 | ```php 435 | $repository->forgetCache(); 436 | ``` 437 | 438 | #### `with()` 439 | 440 | The `with` method sets the relationships that should be eager loaded: 441 | 442 | ```php 443 | // Pass a string 444 | $repository->with('relationship'); 445 | 446 | // Or an array 447 | $repository->with(['relationship1', 'relationship2']); 448 | ``` 449 | 450 | #### `where()` 451 | 452 | The `where` method adds a basic where clause to the query: 453 | 454 | ```php 455 | $repository->where('slug', '=', 'example'); 456 | ``` 457 | 458 | #### `whereIn()` 459 | 460 | The `whereIn` method adds a "where in" clause to the query: 461 | 462 | ```php 463 | $repository->whereIn('id', [1, 2, 5, 8]); 464 | ``` 465 | 466 | #### `whereNotIn()` 467 | 468 | The `whereNotIn` method adds a "where not in" clause to the query: 469 | 470 | ```php 471 | $repository->whereNotIn('id', [1, 2, 5, 8]); 472 | ``` 473 | 474 | #### `whereHas()` 475 | 476 | The `whereHas` method adds a "where has relationship" clause to the query: 477 | 478 | ```php 479 | use Illuminate\Database\Eloquent\Builder; 480 | 481 | $repository->whereHas('attachments', function (Builder $builder) use ($attachment) { 482 | $builder->where('attachment_id', $attachment->id); 483 | }); 484 | ``` 485 | 486 | > **Note:** All of the `where*` methods are chainable & could be called multiple times in a single request. It will hold all where clauses in an array internally and apply them all before executing the query. 487 | 488 | #### `offset()` 489 | 490 | The `offset` method sets the "offset" value of the query: 491 | 492 | ```php 493 | $repository->offset(5); 494 | ``` 495 | 496 | #### `limit()` 497 | 498 | The `limit` method sets the "limit" value of the query: 499 | 500 | ```php 501 | $repository->limit(9); 502 | ``` 503 | 504 | #### `orderBy()` 505 | 506 | The `orderBy` method adds an "order by" clause to the query: 507 | 508 | ```php 509 | $repository->orderBy('id', 'asc'); 510 | ``` 511 | 512 | #### `find()` 513 | 514 | The `find` method finds an entity by it's primary key: 515 | 516 | ```php 517 | $entity = $repository->find(1); 518 | ``` 519 | 520 | #### `findOrFail()` 521 | 522 | The `findOrFail()` method finds an entity by its primary key or throw an exception: 523 | 524 | ```php 525 | $entity = $repository->findOrFail(1); 526 | ``` 527 | 528 | #### `findOrNew()` 529 | 530 | The `findOrNew()` method finds an entity by its primary key or return fresh entity instance: 531 | 532 | ```php 533 | $entity = $repository->findOrNew(1); 534 | ``` 535 | 536 | #### `findBy()` 537 | 538 | The `findBy` method finds an entity by one of it's attributes: 539 | 540 | ```php 541 | $entity = $repository->findBy('id', 1); 542 | ``` 543 | 544 | #### `findFirst()` 545 | 546 | The `findFirst` method finds first entity: 547 | 548 | ```php 549 | $firstEntity = $repository->findFirst(); 550 | ``` 551 | 552 | #### `findAll()` 553 | 554 | The `findAll` method finds all entities: 555 | 556 | ```php 557 | $allEntities = $repository->findAll(); 558 | ``` 559 | 560 | #### `paginate()` 561 | 562 | The `paginate` method paginates all entities: 563 | 564 | ```php 565 | $entitiesPagination = $repository->paginate(15, ['*'], 'page', 2); 566 | ``` 567 | As you can guess, this query the first 15 records, in the second page. 568 | 569 | #### `simplePaginate()` 570 | 571 | The `simplePaginate` method paginates all entities into a simple paginator: 572 | 573 | ```php 574 | $entitiesSimplePagination = $repository->simplePaginate(15); 575 | ``` 576 | 577 | #### `findWhere()` 578 | 579 | The `findWhere` method finds all entities matching where conditions: 580 | 581 | ```php 582 | // Matching values with equal '=' operator 583 | $repository->findWhere(['slug', '=', 'example']); 584 | ``` 585 | 586 | #### `findWhereIn()` 587 | 588 | The `findWhereIn` method finds all entities matching whereIn conditions: 589 | 590 | ```php 591 | $includedEntities = $repository->findwhereIn(['id', [1, 2, 5, 8]]); 592 | ``` 593 | 594 | #### `findWhereNotIn()` 595 | 596 | The `findWhereNotIn` method finds all entities matching whereNotIn conditions: 597 | 598 | ```php 599 | $excludedEntities = $repository->findWhereNotIn(['id', [1, 2, 5, 8]]); 600 | ``` 601 | 602 | #### `findWhereHas()` 603 | 604 | The `findWhereHas` method finds all entities matching whereHas conditions: 605 | 606 | ```php 607 | use Illuminate\Database\Eloquent\Builder; 608 | 609 | $entities = $repository->findWhereHas(['attachments', function (Builder $builder) use ($attachment) { 610 | $builder->where('attachment_id', $attachment->id); 611 | }]); 612 | ``` 613 | 614 | > **Notes:** 615 | > - The `findWhereHas` method will return a collection of entities that match the condition inside the closure. If you need to embed the `attachments` relation, in this case, you'll need to call `with()` method before calling `findWhereHas()` like this: `$repository->with('attachments')->findWhereHas([...]);` 616 | > - Signature of all of the `findWhere`, `findWhereIn`, and `findWhereNotIn` methods has been changed since **v2.0.0**. 617 | > - All of the `findWhere`, `findWhereIn`, and `findWhereNotIn` methods utilize the `where`, `whereIn`, and `whereNotIn` methods respectively, and thus takes first argument as an array of same parameters required by the later ones. 618 | > - All of the `find*` methods are could be filtered with preceding `where` clauses, which is chainable by the way. All `where` clauses been hold in an array internally and applied before executing the query. Check the following examples: 619 | 620 | Example of filtered `findAll` method: 621 | ```php 622 | $allFilteredEntities = $repository->where('slug', '=', 'example')->findAll(); 623 | ``` 624 | 625 | Another example of filtered `findFirst` method with chained clauses: 626 | ```php 627 | $allFilteredEntities = $repository->where('name', 'LIKE', '%TEST%')->where('slug', '=', 'example')->findFirst(); 628 | ``` 629 | 630 | #### `create()` 631 | 632 | The `create` method creates a new entity with the given attributes: 633 | ```php 634 | $createdEntity = $repository->create(['name' => 'Example']); 635 | ``` 636 | 637 | #### `update()` 638 | 639 | The `update` method updates an entity with the given attributes: 640 | ```php 641 | $updatedEntity = $repository->update(1, ['name' => 'Example2']); 642 | ``` 643 | 644 | #### `store()` 645 | 646 | The `store` method stores the entity with the given attributes: 647 | ```php 648 | // Existing Entity 649 | $storedEntity = $repository->store(1, ['name' => 'Example2']); 650 | 651 | // New Entity 652 | $storedEntity = $repository->store(null, ['name' => 'Example2']); 653 | ``` 654 | 655 | > **Note:** This method is just an alias for both `create` & `update` methods. It's useful in case where single form is used for both create & update processes. 656 | 657 | #### `delete()` 658 | 659 | The `delete` method deletes an entity with the given id: 660 | ```php 661 | $deletedEntity = $repository->delete(1); 662 | ``` 663 | 664 | #### `beginTransaction()` 665 | 666 | The `beginTransaction` method starts a database transaction: 667 | ```php 668 | $repository->beginTransaction(); 669 | ``` 670 | 671 | #### `commit()` 672 | 673 | The `commit` method commits a database transaction: 674 | ```php 675 | $repository->commit(); 676 | ``` 677 | 678 | #### `rollBack()` 679 | 680 | The `rollback` method rollbacks a database transaction: 681 | ```php 682 | $repository->rollBack(); 683 | ``` 684 | 685 | > **Notes:** 686 | > - All `find*` methods take one more optional parameter for selected attributes. 687 | > - All `set*` methods returns an instance of the current repository, and thus can be chained. 688 | > - `create`, `update`, and `delete` methods always return an array with two values, the first is action status whether it's success or fail as a boolean value, and the other is an instance of the model just operated upon. 689 | > - It's recommended to set IoC container instance, repository model, and repository identifier explicitly through your repository constructor like the above example, but this package is smart enough to guess any missing requirements. [Check Automatic Guessing Section](#automatic-guessing) 690 | 691 | ### Code To An Interface 692 | 693 | As a best practice, it's recommended to code for an interface, specifically for scalable projects. The following example explains how to do so. 694 | 695 | First, create an interface (abstract) for every entity you've: 696 | ```php 697 | use Rinvex\Repository\Contracts\CacheableContract; 698 | use Rinvex\Repository\Contracts\RepositoryContract; 699 | 700 | interface UserRepositoryContract extends RepositoryContract, CacheableContract 701 | { 702 | // 703 | } 704 | ``` 705 | 706 | Second, create a repository (concrete implementation) for every entity you've: 707 | ```php 708 | use Rinvex\Repository\Repositories\EloquentRepository; 709 | 710 | class UserEloquentRepository extends EloquentRepository implements UserRepositoryContract 711 | { 712 | // 713 | } 714 | ``` 715 | 716 | Now in a Laravel Service Provider bind both to the IoC (inside the `register` method): 717 | ```php 718 | $this->app->bind(UserRepositoryContract::class, UserEloquentRepository::class) 719 | ``` 720 | This way we don't have to instantiate the repository manually, and it's easy to switch between multiple implementations. The IoC Container will take care of the required dependencies. 721 | 722 | > **Note:** Checkout Laravel's [Service Providers](https://laravel.com/docs/master/providers) and [Service Container](https://laravel.com/docs/master/container) documentation for further details. 723 | 724 | ### Add Custom Implementation 725 | 726 | Since we're focusing on abstracting the data layer, and we're separating the abstract interface from the concrete implementation, it's easy to add your own implementation. 727 | 728 | Say your domain model uses a web service, or a filesystem data store as it's data source, all you need to do is just extend the `BaseRepository` class, that's it. See: 729 | ```php 730 | class FilesystemRepository extends BaseRepository 731 | { 732 | // Implement here all `RepositoryContract` methods that query/persist data to & from filesystem or whatever datastore 733 | } 734 | ``` 735 | 736 | ### EloquentRepository Fired Events 737 | 738 | Repositories fire events at every action, like `create`, `update`, `delete`. All fired events are prefixed with repository's identifier (you set before in your [repository's constructor](#eloquentrepository)) like the following example: 739 | 740 | - rinvex.repository.uniqueid.entity.created 741 | - rinvex.repository.uniqueid.entity.updated 742 | - rinvex.repository.uniqueid.entity.deleted 743 | 744 | For your convenience, the events suffixed with `.entity.created`, `.entity.updated`, or `.entity.deleted` have listeners that take actions accordingly. Usually we need to flush cache -if enabled & exists- upon every success action. 745 | 746 | There's one more event `rinvex.repository.uniqueid.entity.cache.flushed` that's fired on cache flush. It has no listeners by default, but you may need to listen to it if you've model relations for further actions. 747 | 748 | ### Mandatory Repository Conventions 749 | 750 | Here some conventions important to know while using this package. This package adheres to best practices trying to make development easier for web artisans, and thus it has some conventions for standardization and interoperability. 751 | 752 | - All Fired Events has a unique suffix, like `.entity.created` for example. Note the `.entity.` which is mandatory for automatic event listeners to subscribe to. 753 | 754 | - Default directory structure of any package uses **Rinvex Repository** is as follows: 755 | ``` 756 | ├── config --> config files 757 | | 758 | ├── database 759 | | ├── factories --> database factory files 760 | | ├── migrations --> database migration files 761 | | └── seeds --> database seed files 762 | | 763 | ├── resources 764 | | └── lang 765 | | └── en --> English language files 766 | | 767 | ├── routes --> Routes files 768 | | ├── api.php 769 | | ├── console.php 770 | | └── web.php 771 | | 772 | ├── src --> self explanatory directories 773 | | ├── Console 774 | | | └── Commands 775 | | | 776 | | ├── Http 777 | | | ├── Controllers 778 | | | ├── Middleware 779 | | | └── Requests 780 | | | 781 | | ├── Events 782 | | ├── Exceptions 783 | | ├── Facades 784 | | ├── Jobs 785 | | ├── Listeners 786 | | ├── Models 787 | | ├── Overrides 788 | | ├── Policies 789 | | ├── Providers 790 | | ├── Repositories 791 | | ├── Scopes 792 | | ├── Support 793 | | └── Traits 794 | | 795 | └── composer.json --> composer dependencies file 796 | ``` 797 | 798 | > **Note:** **Rinvex Repository** adheres to [PSR-4: Autoloader](http://www.php-fig.org/psr/psr-4/) and expects other packages that uses it to adhere to the same standard as well. It's required for [Automatic Guessing](#automatic-guessing), such as when repository model is missing, it will be guessed automatically and resolved accordingly, and while that full directory structure might not required, it's the standard for all **Rinvex** packages. 799 | 800 | ### Automatic Guessing 801 | 802 | While it's **recommended** to explicitly set IoC container, repository identifier, and repository model; This package is smart enough to guess any of these required data whenever missing. 803 | 804 | - **IoC Container** `app()` helper is used as a fallback if IoC container instance not provided explicitly. 805 | - **Repository Identifier** It's recommended to set repository identifier as a doted name like `rinvex.repository.uniqueid`, but if it's missing fully qualified repository class name will be used (actually the value of `static::class`). 806 | - **Repository Model** Conventionally repositories are namespaced like this `Rinvex\Demos\Repositories\ItemRepository`, so corresponding model supposed to be namespaced like this `Rinvex\Demos\Models\Item`. That's how this packages guess the model if it's missing according to the [Default Directory Structure](#mandatory-repository-conventions). 807 | 808 | ### Flexible & Granular Caching 809 | 810 | **Rinvex Repository** has a powerful, yet simple and granular caching system, that handles almost every edge case. While you can enable/disable your application's cache as a whole, you have the flexibility to enable/disable cache granularly for every individual query! That gives you the ability to except certain queries from being cached even if the method is normally cached by default or otherwise. 811 | 812 | Let's see what caching levels we can control: 813 | 814 | #### Whole Application Cache 815 | 816 | Checkout Laravel's [Cache](https://laravel.com/docs/master/cache) documentation for more details. 817 | 818 | #### Individual Query Cache 819 | 820 | Change cache per query or disable it: 821 | ```php 822 | // Set cache lifetime for this individual query to 123 seconds 823 | $repository->setCacheLifetime(123); 824 | 825 | // Set cache lifetime for this individual query to forever 826 | $repository->setCacheLifetime(-1); 827 | 828 | // Disable cache for this individual query 829 | $repository->setCacheLifetime(0); 830 | ``` 831 | 832 | Change cache driver per query: 833 | ```php 834 | // Set cache driver for this individual query to redis 835 | $repository->setCacheDriver('redis'); 836 | ``` 837 | 838 | Both `setCacheLifetime` & `setCacheDriver` methods are chainable: 839 | ```php 840 | // Change cache lifetime & driver on runtime 841 | $repository->setCacheLifetime(123)->setCacheDriver('redis')->findAll(); 842 | 843 | // Use default cache lifetime & driver 844 | $repository->findAll(); 845 | ``` 846 | 847 | Unless disabled explicitly, cache is enabled for all repositories by default, and kept for as long as your `rinvex.repository.cache.lifetime` config value, using default application's cache driver `cache.default` (which could be changed per query as well). 848 | 849 | Caching results is totally up to you, while all retrieval `find*` methods have cache enabled by default, you can enable/disable cache for individual queries or control how it's being cached, for how long, and using which driver as you wish. 850 | 851 | #### Temporary Skip Individual HTTP Request Cache 852 | 853 | Lastly, you can skip cache for an individual request by passing the following query string in your URL `skipCache=true`. You can modify this parameter to whatever name you may need through the `rinvex.repository.cache.skip_uri` config option. 854 | 855 | 856 | ## Final Thoughts 857 | 858 | - Since this is an evolving implementation that may change accordingly depending on real-world use cases. 859 | - Repositories intelligently pass missing called methods to the underlying model, so you actually can implement any kind of logic, or even complex queries by utilizing the repository model. 860 | - For more insights about the Active Repository implementation, I've published an article on the topic titled [Active Repository is good & Awesomely Usable](https://blog.omranic.com/active-repository-is-good-awesomely-usable-6991cfd58774), read it if you're interested. 861 | - Repositories utilizes cache tags in a very smart way, even if your chosen cache driver doesn't support it. Repositories will manage it virtually on it's own for precise cache management. Behind scenes it uses a json file to store cache keys. Checkout the `rinvex.repository.cache.keys_file` config option to change file path. 862 | - **Rinvex Repository** follows the FIG PHP Standards Recommendations compliant with the [PSR-1: Basic Coding Standard](http://www.php-fig.org/psr/psr-1/), [PSR-2: Coding Style Guide](http://www.php-fig.org/psr/psr-2/) and [PSR-4: Autoloader](http://www.php-fig.org/psr/psr-4/) to ensure a high level of interoperability between shared PHP code. 863 | - I don't see the benefit of adding a more complex layer by implementing the **Criteria Pattern** for filtration at the moment, rather I'd prefer to keep it as simple as it is now using traditional where clauses since we can achieve same results. (do you've different thoughts? explain please) 864 | 865 | 866 | ## Changelog 867 | 868 | Refer to the [Changelog](CHANGELOG.md) for a full history of the project. 869 | 870 | 871 | ## Support 872 | 873 | The following support channels are available at your fingertips: 874 | 875 | - [Chat on Slack](https://bit.ly/rinvex-slack) 876 | - [Help on Email](mailto:help@rinvex.com) 877 | - [Follow on Twitter](https://twitter.com/rinvex) 878 | 879 | 880 | ## Contributing & Protocols 881 | 882 | Thank you for considering contributing to this project! The contribution guide can be found in [CONTRIBUTING.md](CONTRIBUTING.md). 883 | 884 | Bug reports, feature requests, and pull requests are very welcome. 885 | 886 | - [Versioning](CONTRIBUTING.md#versioning) 887 | - [Pull Requests](CONTRIBUTING.md#pull-requests) 888 | - [Coding Standards](CONTRIBUTING.md#coding-standards) 889 | - [Feature Requests](CONTRIBUTING.md#feature-requests) 890 | - [Git Flow](CONTRIBUTING.md#git-flow) 891 | 892 | 893 | ## Security Vulnerabilities 894 | 895 | If you discover a security vulnerability within this project, please send an e-mail to [help@rinvex.com](help@rinvex.com). All security vulnerabilities will be promptly addressed. 896 | 897 | 898 | ## About Rinvex 899 | 900 | Rinvex is a software solutions startup, specialized in integrated enterprise solutions for SMEs established in Alexandria, Egypt since June 2016. We believe that our drive The Value, The Reach, and The Impact is what differentiates us and unleash the endless possibilities of our philosophy through the power of software. We like to call it Innovation At The Speed Of Life. That’s how we do our share of advancing humanity. 901 | 902 | 903 | ## License 904 | 905 | This software is released under [The MIT License (MIT)](LICENSE). 906 | 907 | (c) 2016-2020 Rinvex LLC, Some rights reserved. 908 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rinvex/laravel-repositories", 3 | "description": "Rinvex Repository is a simple, intuitive, and smart implementation of Active Repository with extremely flexible & granular caching system for Laravel, used to abstract the data layer, making applications more flexible to maintain.", 4 | "type": "library", 5 | "keywords": [ 6 | "rinvex", 7 | "laravel", 8 | "contract", 9 | "interface", 10 | "repository", 11 | "intuitive", 12 | "eloquent", 13 | "granular", 14 | "pattern", 15 | "cache" 16 | ], 17 | "license": "MIT", 18 | "homepage": "https://rinvex.com", 19 | "support": { 20 | "email": "help@rinvex.com", 21 | "issues": "https://github.com/rinvex/laravel-repositories/issues", 22 | "source": "https://github.com/rinvex/laravel-repositories", 23 | "docs": "https://github.com/rinvex/laravel-repositories/blob/master/README.md" 24 | }, 25 | "authors": [ 26 | { 27 | "name": "Rinvex LLC", 28 | "homepage": "https://rinvex.com", 29 | "email": "help@rinvex.com" 30 | }, 31 | { 32 | "name": "Abdelrahman Omran", 33 | "homepage": "https://omranic.com", 34 | "email": "me@omranic.com", 35 | "role": "Project Lead" 36 | }, 37 | { 38 | "name": "The Generous Laravel Community", 39 | "homepage": "https://github.com/rinvex/laravel-repositories/contributors" 40 | } 41 | ], 42 | "require": { 43 | "php": "^7.4.0", 44 | "illuminate/container": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0|^7.0.0", 45 | "illuminate/contracts": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0|^7.0.0", 46 | "illuminate/database": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0|^7.0.0", 47 | "illuminate/events": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0|^7.0.0", 48 | "illuminate/support": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0|^7.0.0" 49 | }, 50 | "require-dev": { 51 | "codedungeon/phpunit-result-printer": "^0.27.0", 52 | "illuminate/config": "~5.4.0|~5.5.0|~5.6.0|~5.7.0|~5.8.0|^6.0.0", 53 | "phpunit/phpunit": "^9.0.0" 54 | }, 55 | "suggest": { 56 | "illuminate/pagination": "Required to paginate the result set." 57 | }, 58 | "autoload": { 59 | "psr-4": { 60 | "Rinvex\\Repository\\": "src/" 61 | } 62 | }, 63 | "autoload-dev": { 64 | "psr-4": { 65 | "Rinvex\\Repository\\Tests\\": "tests" 66 | } 67 | }, 68 | "scripts": { 69 | "test": "vendor/bin/phpunit" 70 | }, 71 | "config": { 72 | "sort-packages": true, 73 | "preferred-install": "dist", 74 | "optimize-autoloader": true 75 | }, 76 | "extra": { 77 | "laravel": { 78 | "providers": [ 79 | "Rinvex\\Repository\\Providers\\RepositoryServiceProvider" 80 | ] 81 | } 82 | }, 83 | "minimum-stability": "dev", 84 | "prefer-stable": true 85 | } 86 | -------------------------------------------------------------------------------- /config/config.php: -------------------------------------------------------------------------------- 1 | 'Models', 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Caching Strategy 24 | |-------------------------------------------------------------------------- 25 | */ 26 | 27 | 'cache' => [ 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Cache Keys File 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Here you may specify the cache keys file that is used only with cache 35 | | drivers that does not support cache tags. It is mandatory to keep 36 | | track of cache keys for later usage on cache flush process. 37 | | 38 | | Default: storage_path('framework/cache/rinvex.repository.json') 39 | | 40 | */ 41 | 42 | 'keys_file' => storage_path('framework/cache/rinvex.repository.json'), 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Cache Lifetime 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Here you may specify the number of minutes that you wish the cache 50 | | to be remembered before it expires. If you want the cache to be 51 | | remembered forever, set this option to -1. 0 means disabled. 52 | | 53 | | Default: -1 54 | | 55 | */ 56 | 57 | 'lifetime' => -1, 58 | 59 | /* 60 | |-------------------------------------------------------------------------- 61 | | Cache Clear 62 | |-------------------------------------------------------------------------- 63 | | 64 | | Specify which actions would you like to clear cache upon success. 65 | | All repository cached data will be cleared accordingly. 66 | | 67 | | Default: ['create', 'update', 'delete'] 68 | | 69 | */ 70 | 71 | 'clear_on' => [ 72 | 'create', 73 | 'update', 74 | 'delete', 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Cache Skipping URI 80 | |-------------------------------------------------------------------------- 81 | | 82 | | For testing purposes, or maybe some certain situations, you may wish 83 | | to skip caching layer and get fresh data result set just for the 84 | | current request. This option allows you to specify custom 85 | | URL parameter for skipping caching layer easily. 86 | | 87 | | Default: 'skipCache' 88 | | 89 | */ 90 | 91 | 'skip_uri' => 'skipCache', 92 | 93 | ], 94 | 95 | ]; 96 | -------------------------------------------------------------------------------- /phpstan.neon.dist: -------------------------------------------------------------------------------- 1 | includes: 2 | - ./vendor/nunomaduro/larastan/extension.neon 3 | parameters: 4 | level: 5 5 | paths: 6 | - src 7 | -------------------------------------------------------------------------------- /src/Contracts/CacheableContract.php: -------------------------------------------------------------------------------- 1 | =', $count = 1); 142 | 143 | /** 144 | * Add a scope to the query. 145 | * 146 | * @param string $name 147 | * @param array $parameters 148 | * 149 | * @return static 150 | */ 151 | public function scope($name, array $parameters = []); 152 | 153 | /** 154 | * Set the "offset" value of the query. 155 | * 156 | * @param int $offset 157 | * 158 | * @return static 159 | */ 160 | public function offset($offset); 161 | 162 | /** 163 | * Set the "limit" value of the query. 164 | * 165 | * @param int $limit 166 | * 167 | * @return static 168 | */ 169 | public function limit($limit); 170 | 171 | /** 172 | * Add an "order by" clause to the query. 173 | * 174 | * @param string $attribute 175 | * @param string $direction 176 | * 177 | * @return static 178 | */ 179 | public function orderBy($attribute, $direction = 'asc'); 180 | 181 | /** 182 | * Add a "group by" clause to the query. 183 | * 184 | * @param array|string $column 185 | * 186 | * @return static 187 | */ 188 | public function groupBy($column); 189 | 190 | /** 191 | * Add a "having" clause to the query. 192 | * 193 | * @param string $column 194 | * @param string $operator 195 | * @param string $value 196 | * @param string $boolean 197 | * 198 | * @return static 199 | */ 200 | public function having($column, $operator = null, $value = null, $boolean = 'and'); 201 | 202 | /** 203 | * Add a "or having" clause to the query. 204 | * 205 | * @param string $column 206 | * @param string $operator 207 | * @param string $value 208 | * 209 | * @return static 210 | */ 211 | public function orHaving($column, $operator = null, $value = null); 212 | 213 | /** 214 | * Find an entity by its primary key. 215 | * 216 | * @param int $id 217 | * @param array $attributes 218 | * 219 | * @return mixed 220 | */ 221 | public function find($id, $attributes = ['*']); 222 | 223 | /** 224 | * Find an entity by its primary key or throw an exception. 225 | * 226 | * @param mixed $id 227 | * @param array $attributes 228 | * 229 | * @throws \RuntimeException 230 | * 231 | * @return mixed 232 | */ 233 | public function findOrFail($id, $attributes = ['*']); 234 | 235 | /** 236 | * Find an entity by its primary key or return fresh entity instance. 237 | * 238 | * @param mixed $id 239 | * @param array $attributes 240 | * 241 | * @return mixed 242 | */ 243 | public function findOrNew($id, $attributes = ['*']); 244 | 245 | /** 246 | * Find an entity by one of its attributes. 247 | * 248 | * @param string $attribute 249 | * @param string $value 250 | * @param array $attributes 251 | * 252 | * @return mixed 253 | */ 254 | public function findBy($attribute, $value, $attributes = ['*']); 255 | 256 | /** 257 | * Find the first entity. 258 | * 259 | * @param array $attributes 260 | * 261 | * @return mixed 262 | */ 263 | public function findFirst($attributes = ['*']); 264 | 265 | /** 266 | * Find all entities. 267 | * 268 | * @param array $attributes 269 | * 270 | * @return \Illuminate\Support\Collection 271 | */ 272 | public function findAll($attributes = ['*']); 273 | 274 | /** 275 | * Paginate all entities. 276 | * 277 | * @param int|null $perPage 278 | * @param array $attributes 279 | * @param string $pageName 280 | * @param int|null $page 281 | * 282 | * @throws \InvalidArgumentException 283 | * 284 | * @return \Illuminate\Contracts\Pagination\LengthAwarePaginator 285 | */ 286 | public function paginate($perPage = null, $attributes = ['*'], $pageName = 'page', $page = null); 287 | 288 | /** 289 | * Paginate all entities into a simple paginator. 290 | * 291 | * @param int|null $perPage 292 | * @param array $attributes 293 | * @param string $pageName 294 | * 295 | * @return \Illuminate\Contracts\Pagination\Paginator 296 | */ 297 | public function simplePaginate($perPage = null, $attributes = ['*'], $pageName = 'page'); 298 | 299 | /** 300 | * Find all entities matching where conditions. 301 | * 302 | * @param array $where 303 | * @param array $attributes 304 | * 305 | * @return \Illuminate\Support\Collection 306 | */ 307 | public function findWhere(array $where, $attributes = ['*']); 308 | 309 | /** 310 | * Find all entities matching whereIn conditions. 311 | * 312 | * @param array $where 313 | * @param array $attributes 314 | * 315 | * @return \Illuminate\Support\Collection 316 | */ 317 | public function findWhereIn(array $where, $attributes = ['*']); 318 | 319 | /** 320 | * Find all entities matching whereNotIn conditions. 321 | * 322 | * @param array $where 323 | * @param array $attributes 324 | * 325 | * @return \Illuminate\Support\Collection 326 | */ 327 | public function findWhereNotIn(array $where, $attributes = ['*']); 328 | 329 | /** 330 | * Find all entities matching whereHas conditions. 331 | * 332 | * @param array $where 333 | * @param array $attributes 334 | * 335 | * @return \Illuminate\Support\Collection 336 | */ 337 | public function findWhereHas(array $where, $attributes = ['*']); 338 | 339 | /** 340 | * Create a new entity with the given attributes. 341 | * 342 | * @param array $attributes 343 | * @param bool $syncRelations 344 | * 345 | * @return mixed 346 | */ 347 | public function create(array $attributes = [], bool $syncRelations = false); 348 | 349 | /** 350 | * Update an entity with the given attributes. 351 | * 352 | * @param mixed $id 353 | * @param array $attributes 354 | * @param bool $syncRelations 355 | * 356 | * @return mixed 357 | */ 358 | public function update($id, array $attributes = [], bool $syncRelations = false); 359 | 360 | /** 361 | * Store the entity with the given attributes. 362 | * 363 | * @param mixed $id 364 | * @param array $attributes 365 | * @param bool $syncRelations 366 | * 367 | * @return mixed 368 | */ 369 | public function store($id, array $attributes = [], bool $syncRelations = false); 370 | 371 | /** 372 | * Delete an entity with the given id. 373 | * 374 | * @param mixed $id 375 | * 376 | * @return mixed 377 | */ 378 | public function delete($id); 379 | 380 | /** 381 | * Restore an entity with the given id. 382 | * 383 | * @param mixed $id 384 | * 385 | * @return mixed 386 | */ 387 | public function restore($id); 388 | 389 | /** 390 | * Start a new database transaction. 391 | * 392 | * @throws \Exception 393 | * 394 | * @return void 395 | */ 396 | public function beginTransaction(): void; 397 | 398 | /** 399 | * Commit the active database transaction. 400 | * 401 | * @return void 402 | */ 403 | public function commit(): void; 404 | 405 | /** 406 | * Rollback the active database transaction. 407 | * 408 | * @return void 409 | */ 410 | public function rollBack(): void; 411 | 412 | /** 413 | * Retrieve the "count" result of the query. 414 | * 415 | * @param string $columns 416 | * 417 | * @return int 418 | */ 419 | public function count($columns = '*'): int; 420 | 421 | /** 422 | * Retrieve the minimum value of a given column. 423 | * 424 | * @param string $column 425 | * 426 | * @return mixed 427 | */ 428 | public function min($column); 429 | 430 | /** 431 | * Retrieve the maximum value of a given column. 432 | * 433 | * @param string $column 434 | * 435 | * @return mixed 436 | */ 437 | public function max($column); 438 | 439 | /** 440 | * Retrieve the average value of a given column. 441 | * 442 | * @param string $column 443 | * 444 | * @return mixed 445 | */ 446 | public function avg($column); 447 | 448 | /** 449 | * Retrieve the sum of the values of a given column. 450 | * 451 | * @param string $column 452 | * 453 | * @return mixed 454 | */ 455 | public function sum($column); 456 | 457 | /** 458 | * Dynamically pass missing static methods to the model. 459 | * 460 | * @param $method 461 | * @param $parameters 462 | * 463 | * @return mixed 464 | */ 465 | public static function __callStatic($method, $parameters); 466 | 467 | /** 468 | * Dynamically pass missing methods to the model. 469 | * 470 | * @param string $method 471 | * @param array $parameters 472 | * 473 | * @return mixed 474 | */ 475 | public function __call($method, $parameters); 476 | } 477 | -------------------------------------------------------------------------------- /src/Exceptions/CriterionException.php: -------------------------------------------------------------------------------- 1 | id = $id; 36 | $this->model = $model; 37 | $this->message = "No results for model [{$model}] #{$id}."; 38 | } 39 | 40 | /** 41 | * Get the affected model. 42 | * 43 | * @return string 44 | */ 45 | public function getModel(): string 46 | { 47 | return $this->model; 48 | } 49 | 50 | /** 51 | * Get the affected model Id. 52 | * 53 | * @return string 54 | */ 55 | public function getId(): string 56 | { 57 | return $this->id; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exceptions/RepositoryException.php: -------------------------------------------------------------------------------- 1 | listen('*.entity.creating', self::class.'@entityCreating'); 19 | $dispatcher->listen('*.entity.created', self::class.'@entityCreated'); 20 | $dispatcher->listen('*.entity.updating', self::class.'@entityUpdating'); 21 | $dispatcher->listen('*.entity.updated', self::class.'@entityUpdated'); 22 | $dispatcher->listen('*.entity.deleting', self::class.'@entityDeleting'); 23 | $dispatcher->listen('*.entity.deleted', self::class.'@entityDeleted'); 24 | } 25 | 26 | /** 27 | * Listen to entities being created. 28 | * 29 | * @param string $eventName 30 | * @param array $data 31 | * 32 | * @return void 33 | */ 34 | public function entityCreating($eventName, $data): void 35 | { 36 | // 37 | } 38 | 39 | /** 40 | * Listen to entities created. 41 | * 42 | * @param string $eventName 43 | * @param array $data 44 | * 45 | * @return void 46 | */ 47 | public function entityCreated($eventName, $data): void 48 | { 49 | $clearOn = $data[0]->getContainer('config')->get('rinvex.repository.cache.clear_on'); 50 | 51 | if ($data[0]->isCacheClearEnabled() && in_array('create', $clearOn)) { 52 | $data[0]->forgetCache(); 53 | } 54 | } 55 | 56 | /** 57 | * Listen to entities being updated. 58 | * 59 | * @param string $eventName 60 | * @param array $data 61 | * 62 | * @return void 63 | */ 64 | public function entityUpdating($eventName, $data): void 65 | { 66 | // 67 | } 68 | 69 | /** 70 | * Listen to entities updated. 71 | * 72 | * @param string $eventName 73 | * @param array $data 74 | * 75 | * @return void 76 | */ 77 | public function entityUpdated($eventName, $data): void 78 | { 79 | $clearOn = $data[0]->getContainer('config')->get('rinvex.repository.cache.clear_on'); 80 | 81 | if ($data[0]->isCacheClearEnabled() && in_array('update', $clearOn)) { 82 | $data[0]->forgetCache(); 83 | } 84 | } 85 | 86 | /** 87 | * Listen to entities being deleted. 88 | * 89 | * @param string $eventName 90 | * @param array $data 91 | * 92 | * @return void 93 | */ 94 | public function entityDeleting($eventName, $data): void 95 | { 96 | // 97 | } 98 | 99 | /** 100 | * Listen to entities deleted. 101 | * 102 | * @param string $eventName 103 | * @param array $data 104 | * 105 | * @return void 106 | */ 107 | public function entityDeleted($eventName, $data): void 108 | { 109 | $clearOn = $data[0]->getContainer('config')->get('rinvex.repository.cache.clear_on'); 110 | 111 | if ($data[0]->isCacheClearEnabled() && in_array('delete', $clearOn)) { 112 | $data[0]->forgetCache(); 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/Providers/RepositoryServiceProvider.php: -------------------------------------------------------------------------------- 1 | mergeConfigFrom(realpath(__DIR__.'/../../config/config.php'), 'rinvex.repository'); 29 | 30 | // Register the event listener 31 | $this->app->bind('rinvex.repository.listener', RepositoryEventListener::class); 32 | } 33 | 34 | /** 35 | * {@inheritdoc} 36 | */ 37 | public function boot() 38 | { 39 | // Publish config 40 | $this->publishesConfig('rinvex/laravel-repositories'); 41 | 42 | // Subscribe the registered event listener 43 | $this->app['events']->subscribe('rinvex.repository.listener'); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Repositories/BaseRepository.php: -------------------------------------------------------------------------------- 1 | getContainer('config')->get('rinvex.repository.cache.skip_uri'); 135 | 136 | // Check if cache is enabled 137 | if ($this->getCacheLifetime() && ! $this->getContainer('request')->has($skipUri)) { 138 | return $this->cacheCallback($class, $method, $args, $closure); 139 | } 140 | 141 | // Cache disabled, just execute query & return result 142 | $result = call_user_func($closure); 143 | 144 | // We're done, let's clean up! 145 | $this->resetRepository(); 146 | 147 | return $result; 148 | } 149 | 150 | /** 151 | * Reset repository to its defaults. 152 | * 153 | * @return $this 154 | */ 155 | protected function resetRepository() 156 | { 157 | $this->relations = []; 158 | $this->where = []; 159 | $this->whereIn = []; 160 | $this->whereNotIn = []; 161 | $this->whereHas = []; 162 | $this->scopes = []; 163 | $this->offset = null; 164 | $this->limit = null; 165 | $this->orderBy = []; 166 | $this->groupBy = []; 167 | $this->having = []; 168 | 169 | if (method_exists($this, 'flushCriteria')) { 170 | $this->flushCriteria(); 171 | } 172 | 173 | return $this; 174 | } 175 | 176 | /** 177 | * Prepare query. 178 | * 179 | * @param object $model 180 | * 181 | * @return mixed 182 | */ 183 | protected function prepareQuery($model) 184 | { 185 | // Set the relationships that should be eager loaded 186 | if (! empty($this->relations)) { 187 | $model = $model->with($this->relations); 188 | } 189 | 190 | // Add a basic where clause to the query 191 | foreach ($this->where as $where) { 192 | [$attribute, $operator, $value, $boolean] = array_pad($where, 4, null); 193 | 194 | $model = $model->where($attribute, $operator, $value, $boolean); 195 | } 196 | 197 | // Add a "where in" clause to the query 198 | foreach ($this->whereIn as $whereIn) { 199 | [$attribute, $values, $boolean, $not] = array_pad($whereIn, 4, null); 200 | 201 | $model = $model->whereIn($attribute, $values, $boolean, $not); 202 | } 203 | 204 | // Add a "where not in" clause to the query 205 | foreach ($this->whereNotIn as $whereNotIn) { 206 | [$attribute, $values, $boolean] = array_pad($whereNotIn, 3, null); 207 | 208 | $model = $model->whereNotIn($attribute, $values, $boolean); 209 | } 210 | 211 | // Add a "where has" clause to the query 212 | foreach ($this->whereHas as $whereHas) { 213 | [$relation, $callback, $operator, $count] = array_pad($whereHas, 4, null); 214 | 215 | $model = $model->whereHas($relation, $callback, $operator, $count); 216 | } 217 | 218 | // Add a "scope" to the query 219 | foreach ($this->scopes as $scope => $parameters) { 220 | $model = $model->{$scope}(...$parameters); 221 | } 222 | 223 | // Set the "offset" value of the query 224 | if ($this->offset > 0) { 225 | $model = $model->offset($this->offset); 226 | } 227 | 228 | // Set the "limit" value of the query 229 | if ($this->limit > 0) { 230 | $model = $model->limit($this->limit); 231 | } 232 | 233 | // Add an "order by" clause to the query. 234 | foreach ($this->orderBy as $orderBy) { 235 | [$attribute, $direction] = $orderBy; 236 | 237 | $model = $model->orderBy($attribute, $direction); 238 | } 239 | 240 | // Add an "group by" clause to the query. 241 | if (! empty($this->groupBy)) { 242 | foreach ($this->groupBy as $group) { 243 | $model = $model->groupBy($group); 244 | } 245 | } 246 | 247 | // Add a "having" clause to the query 248 | foreach ($this->having as $having) { 249 | [$column, $operator, $value, $boolean] = array_pad($having, 4, null); 250 | 251 | $model = $model->having($column, $operator, $value, $boolean); 252 | } 253 | 254 | // Apply all criteria to the query 255 | if (method_exists($this, 'applyCriteria')) { 256 | $model = $this->applyCriteria($model, $this); 257 | } 258 | 259 | return $model; 260 | } 261 | 262 | /** 263 | * {@inheritdoc} 264 | */ 265 | public function setContainer(Container $container) 266 | { 267 | $this->container = $container; 268 | 269 | return $this; 270 | } 271 | 272 | /** 273 | * {@inheritdoc} 274 | */ 275 | public function getContainer($service = null) 276 | { 277 | return is_null($service) ? ($this->container ?: app()) : ($this->container[$service] ?: app($service)); 278 | } 279 | 280 | /** 281 | * {@inheritdoc} 282 | */ 283 | public function setConnection($name) 284 | { 285 | $this->connection = $name; 286 | 287 | return $this; 288 | } 289 | 290 | /** 291 | * {@inheritdoc} 292 | */ 293 | public function getConnection(): string 294 | { 295 | return $this->connection; 296 | } 297 | 298 | /** 299 | * {@inheritdoc} 300 | */ 301 | public function setRepositoryId($repositoryId) 302 | { 303 | $this->repositoryId = $repositoryId; 304 | 305 | return $this; 306 | } 307 | 308 | /** 309 | * {@inheritdoc} 310 | */ 311 | public function getRepositoryId(): string 312 | { 313 | return $this->repositoryId ?: static::class; 314 | } 315 | 316 | /** 317 | * {@inheritdoc} 318 | */ 319 | public function setModel($model) 320 | { 321 | $this->model = $model; 322 | 323 | return $this; 324 | } 325 | 326 | /** 327 | * {@inheritdoc} 328 | */ 329 | public function getModel(): string 330 | { 331 | $model = $this->getContainer('config')->get('rinvex.repository.models'); 332 | 333 | return $this->model ?: str_replace(['Repositories', 'Repository'], [$model, ''], static::class); 334 | } 335 | 336 | /** 337 | * {@inheritdoc} 338 | */ 339 | public function with($relations) 340 | { 341 | if (is_string($relations)) { 342 | $relations = func_get_args(); 343 | } 344 | 345 | $this->relations = $relations; 346 | 347 | return $this; 348 | } 349 | 350 | /** 351 | * {@inheritdoc} 352 | */ 353 | public function where($attribute, $operator = null, $value = null, $boolean = 'and') 354 | { 355 | // The last `$boolean` expression is intentional to fix list() & array_pad() results 356 | $this->where[] = [$attribute, $operator, $value, $boolean ?: 'and']; 357 | 358 | return $this; 359 | } 360 | 361 | /** 362 | * {@inheritdoc} 363 | */ 364 | public function whereIn($attribute, $values, $boolean = 'and', $not = false) 365 | { 366 | // The last `$boolean` & `$not` expressions are intentional to fix list() & array_pad() results 367 | $this->whereIn[] = [$attribute, $values, $boolean ?: 'and', (bool) $not]; 368 | 369 | return $this; 370 | } 371 | 372 | /** 373 | * {@inheritdoc} 374 | */ 375 | public function whereNotIn($attribute, $values, $boolean = 'and') 376 | { 377 | // The last `$boolean` expression is intentional to fix list() & array_pad() results 378 | $this->whereNotIn[] = [$attribute, $values, $boolean ?: 'and']; 379 | 380 | return $this; 381 | } 382 | 383 | /** 384 | * {@inheritdoc} 385 | */ 386 | public function whereHas($relation, Closure $callback = null, $operator = '>=', $count = 1) 387 | { 388 | // The last `$operator` & `$count` expressions are intentional to fix list() & array_pad() results 389 | $this->whereHas[] = [$relation, $callback, $operator ?: '>=', $count ?: 1]; 390 | 391 | return $this; 392 | } 393 | 394 | /** 395 | * {@inheritdoc} 396 | */ 397 | public function scope($name, array $parameters = []) 398 | { 399 | $this->scopes[$name] = $parameters; 400 | 401 | return $this; 402 | } 403 | 404 | /** 405 | * {@inheritdoc} 406 | */ 407 | public function offset($offset) 408 | { 409 | $this->offset = $offset; 410 | 411 | return $this; 412 | } 413 | 414 | /** 415 | * {@inheritdoc} 416 | */ 417 | public function limit($limit) 418 | { 419 | $this->limit = $limit; 420 | 421 | return $this; 422 | } 423 | 424 | /** 425 | * {@inheritdoc} 426 | */ 427 | public function orderBy($attribute, $direction = 'asc') 428 | { 429 | $this->orderBy[] = [$attribute, $direction ?: 'asc']; 430 | 431 | return $this; 432 | } 433 | 434 | /** 435 | * {@inheritdoc} 436 | */ 437 | public function groupBy($column) 438 | { 439 | $this->groupBy = array_merge((array) $this->groupBy, is_array($column) ? $column : [$column]); 440 | 441 | return $this; 442 | } 443 | 444 | /** 445 | * {@inheritdoc} 446 | */ 447 | public function having($column, $operator = null, $value = null, $boolean = 'and') 448 | { 449 | $this->having[] = [$column, $operator, $value, $boolean ?: 'and']; 450 | 451 | return $this; 452 | } 453 | 454 | /** 455 | * {@inheritdoc} 456 | */ 457 | public function orHaving($column, $operator = null, $value = null, $boolean = 'and') 458 | { 459 | return $this->having($column, $operator, $value, 'or'); 460 | } 461 | 462 | /** 463 | * {@inheritdoc} 464 | */ 465 | public function store($id, array $attributes = [], bool $syncRelations = false) 466 | { 467 | return ! $id ? $this->create($attributes, $syncRelations) : $this->update($id, $attributes, $syncRelations); 468 | } 469 | 470 | /** 471 | * {@inheritdoc} 472 | */ 473 | public static function __callStatic($method, $parameters) 474 | { 475 | return call_user_func_array([new static(), $method], $parameters); 476 | } 477 | 478 | /** 479 | * {@inheritdoc} 480 | */ 481 | public function __call($method, $parameters) 482 | { 483 | if (method_exists($model = $this->createModel(), $scope = 'scope'.ucfirst($method))) { 484 | $this->scope($method, $parameters); 485 | 486 | return $this; 487 | } 488 | 489 | return call_user_func_array([$this->createModel(), $method], $parameters); 490 | } 491 | } 492 | -------------------------------------------------------------------------------- /src/Repositories/EloquentRepository.php: -------------------------------------------------------------------------------- 1 | getModel())) { 21 | if (! class_exists($class = '\\'.ltrim($model, '\\'))) { 22 | throw new RepositoryException("Class {$model} does NOT exist!"); 23 | } 24 | 25 | $model = $this->getContainer()->make($class); 26 | } 27 | 28 | // Set the connection used by the model 29 | if (! empty($this->connection)) { 30 | $model = $model->setConnection($this->connection); 31 | } 32 | 33 | if (! $model instanceof Model) { 34 | throw new RepositoryException("Class {$model} must be an instance of \\Illuminate\\Database\\Eloquent\\Model"); 35 | } 36 | 37 | return $model; 38 | } 39 | 40 | /** 41 | * {@inheritdoc} 42 | */ 43 | public function find($id, $attributes = ['*']) 44 | { 45 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($id, $attributes) { 46 | return $this->prepareQuery($this->createModel())->find($id, $attributes); 47 | }); 48 | } 49 | 50 | /** 51 | * {@inheritdoc} 52 | */ 53 | public function findOrFail($id, $attributes = ['*']) 54 | { 55 | $result = $this->find($id, $attributes); 56 | 57 | if (is_array($id)) { 58 | if (count($result) === count(array_unique($id))) { 59 | return $result; 60 | } 61 | } elseif (! is_null($result)) { 62 | return $result; 63 | } 64 | 65 | throw new EntityNotFoundException($this->getModel(), $id); 66 | } 67 | 68 | /** 69 | * {@inheritdoc} 70 | */ 71 | public function findOrNew($id, $attributes = ['*']) 72 | { 73 | if (! is_null($entity = $this->find($id, $attributes))) { 74 | return $entity; 75 | } 76 | 77 | return $this->createModel(); 78 | } 79 | 80 | /** 81 | * {@inheritdoc} 82 | */ 83 | public function findBy($attribute, $value, $attributes = ['*']) 84 | { 85 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($attribute, $value, $attributes) { 86 | return $this->prepareQuery($this->createModel())->where($attribute, '=', $value)->first($attributes); 87 | }); 88 | } 89 | 90 | /** 91 | * {@inheritdoc} 92 | */ 93 | public function findFirst($attributes = ['*']) 94 | { 95 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($attributes) { 96 | return $this->prepareQuery($this->createModel())->first($attributes); 97 | }); 98 | } 99 | 100 | /** 101 | * {@inheritdoc} 102 | */ 103 | public function findAll($attributes = ['*']) 104 | { 105 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($attributes) { 106 | return $this->prepareQuery($this->createModel())->get($attributes); 107 | }); 108 | } 109 | 110 | /** 111 | * {@inheritdoc} 112 | */ 113 | public function paginate($perPage = null, $attributes = ['*'], $pageName = 'page', $page = null) 114 | { 115 | $page = $page ?: Paginator::resolveCurrentPage($pageName); 116 | 117 | return $this->executeCallback(static::class, __FUNCTION__, array_merge(func_get_args(), compact('page')), function () use ($perPage, $attributes, $pageName, $page) { 118 | return $this->prepareQuery($this->createModel())->paginate($perPage, $attributes, $pageName, $page); 119 | }); 120 | } 121 | 122 | /** 123 | * {@inheritdoc} 124 | */ 125 | public function simplePaginate($perPage = null, $attributes = ['*'], $pageName = 'page', $page = null) 126 | { 127 | $page = $page ?: Paginator::resolveCurrentPage($pageName); 128 | 129 | return $this->executeCallback(static::class, __FUNCTION__, array_merge(func_get_args(), compact('page')), function () use ($perPage, $attributes, $pageName, $page) { 130 | return $this->prepareQuery($this->createModel())->simplePaginate($perPage, $attributes, $pageName, $page); 131 | }); 132 | } 133 | 134 | /** 135 | * {@inheritdoc} 136 | */ 137 | public function findWhere(array $where, $attributes = ['*']) 138 | { 139 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($where, $attributes) { 140 | [$attribute, $operator, $value, $boolean] = array_pad($where, 4, null); 141 | 142 | $this->where($attribute, $operator, $value, $boolean); 143 | 144 | return $this->prepareQuery($this->createModel())->get($attributes); 145 | }); 146 | } 147 | 148 | /** 149 | * {@inheritdoc} 150 | */ 151 | public function findWhereIn(array $where, $attributes = ['*']) 152 | { 153 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($where, $attributes) { 154 | [$attribute, $values, $boolean, $not] = array_pad($where, 4, null); 155 | 156 | $this->whereIn($attribute, $values, $boolean, $not); 157 | 158 | return $this->prepareQuery($this->createModel())->get($attributes); 159 | }); 160 | } 161 | 162 | /** 163 | * {@inheritdoc} 164 | */ 165 | public function findWhereNotIn(array $where, $attributes = ['*']) 166 | { 167 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($where, $attributes) { 168 | [$attribute, $values, $boolean] = array_pad($where, 3, null); 169 | 170 | $this->whereNotIn($attribute, $values, $boolean); 171 | 172 | return $this->prepareQuery($this->createModel())->get($attributes); 173 | }); 174 | } 175 | 176 | /** 177 | * {@inheritdoc} 178 | */ 179 | public function findWhereHas(array $where, $attributes = ['*']) 180 | { 181 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($where, $attributes) { 182 | [$relation, $callback, $operator, $count] = array_pad($where, 4, null); 183 | 184 | $this->whereHas($relation, $callback, $operator, $count); 185 | 186 | return $this->prepareQuery($this->createModel())->get($attributes); 187 | }); 188 | } 189 | 190 | /** 191 | * {@inheritdoc} 192 | */ 193 | public function create(array $attributes = [], bool $syncRelations = false) 194 | { 195 | // Create a new instance 196 | $entity = $this->createModel(); 197 | 198 | // Fire the created event 199 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.creating', [$this, $entity]); 200 | 201 | // Extract relationships 202 | if ($syncRelations) { 203 | $relations = $this->extractRelations($entity, $attributes); 204 | Arr::forget($attributes, array_keys($relations)); 205 | } 206 | 207 | // Fill instance with data 208 | $entity->fill($attributes); 209 | 210 | // Save the instance 211 | $created = $entity->save(); 212 | 213 | // Sync relationships 214 | if ($syncRelations && isset($relations)) { 215 | $this->syncRelations($entity, $relations); 216 | } 217 | 218 | // Fire the created event 219 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.created', [$this, $entity]); 220 | 221 | // Return instance 222 | return $created ? $entity : $created; 223 | } 224 | 225 | /** 226 | * {@inheritdoc} 227 | */ 228 | public function update($id, array $attributes = [], bool $syncRelations = false) 229 | { 230 | $updated = false; 231 | 232 | // Find the given instance 233 | $entity = $id instanceof Model ? $id : $this->find($id); 234 | 235 | if ($entity) { 236 | // Fire the updated event 237 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.updating', [$this, $entity]); 238 | 239 | // Extract relationships 240 | if ($syncRelations) { 241 | $relations = $this->extractRelations($entity, $attributes); 242 | Arr::forget($attributes, array_keys($relations)); 243 | } 244 | 245 | // Fill instance with data 246 | $entity->fill($attributes); 247 | 248 | //Check if we are updating attributes values 249 | $dirty = $entity->getDirty(); 250 | 251 | // Update the instance 252 | $updated = $entity->save(); 253 | 254 | // Sync relationships 255 | if ($syncRelations && isset($relations)) { 256 | $this->syncRelations($entity, $relations); 257 | } 258 | 259 | if (count($dirty) > 0) { 260 | // Fire the updated event 261 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.updated', [$this, $entity]); 262 | } 263 | } 264 | 265 | return $updated ? $entity : $updated; 266 | } 267 | 268 | /** 269 | * {@inheritdoc} 270 | */ 271 | public function delete($id) 272 | { 273 | $deleted = false; 274 | 275 | // Find the given instance 276 | $entity = $id instanceof Model ? $id : $this->find($id); 277 | 278 | if ($entity) { 279 | // Fire the deleted event 280 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.deleting', [$this, $entity]); 281 | 282 | // Delete the instance 283 | $deleted = $entity->delete(); 284 | 285 | // Fire the deleted event 286 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.deleted', [$this, $entity]); 287 | } 288 | 289 | return $deleted ? $entity : $deleted; 290 | } 291 | 292 | /** 293 | * {@inheritdoc} 294 | */ 295 | public function restore($id) 296 | { 297 | $restored = false; 298 | 299 | // Find the given instance 300 | $entity = $id instanceof Model ? $id : $this->find($id); 301 | 302 | if ($entity) { 303 | // Fire the restoring event 304 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.restoring', [$this, $entity]); 305 | 306 | // Restore the instance 307 | $restored = $entity->restore(); 308 | 309 | // Fire the restored event 310 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.restored', [$this, $entity]); 311 | } 312 | 313 | return $restored ? $entity : $restored; 314 | } 315 | 316 | /** 317 | * {@inheritdoc} 318 | */ 319 | public function beginTransaction(): void 320 | { 321 | $this->getContainer('db')->beginTransaction(); 322 | } 323 | 324 | /** 325 | * {@inheritdoc} 326 | */ 327 | public function commit(): void 328 | { 329 | $this->getContainer('db')->commit(); 330 | } 331 | 332 | /** 333 | * {@inheritdoc} 334 | */ 335 | public function rollBack(): void 336 | { 337 | $this->getContainer('db')->rollBack(); 338 | } 339 | 340 | /** 341 | * {@inheritdoc} 342 | */ 343 | public function count($columns = '*'): int 344 | { 345 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($columns) { 346 | return $this->prepareQuery($this->createModel())->count($columns); 347 | }); 348 | } 349 | 350 | /** 351 | * {@inheritdoc} 352 | */ 353 | public function min($column) 354 | { 355 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($column) { 356 | return $this->prepareQuery($this->createModel())->min($column); 357 | }); 358 | } 359 | 360 | /** 361 | * {@inheritdoc} 362 | */ 363 | public function max($column) 364 | { 365 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($column) { 366 | return $this->prepareQuery($this->createModel())->max($column); 367 | }); 368 | } 369 | 370 | /** 371 | * {@inheritdoc} 372 | */ 373 | public function avg($column) 374 | { 375 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($column) { 376 | return $this->prepareQuery($this->createModel())->avg($column); 377 | }); 378 | } 379 | 380 | /** 381 | * {@inheritdoc} 382 | */ 383 | public function sum($column) 384 | { 385 | return $this->executeCallback(static::class, __FUNCTION__, func_get_args(), function () use ($column) { 386 | return $this->prepareQuery($this->createModel())->sum($column); 387 | }); 388 | } 389 | 390 | /** 391 | * Extract relationships. 392 | * 393 | * @param mixed $entity 394 | * @param array $attributes 395 | * 396 | * @return array 397 | */ 398 | protected function extractRelations($entity, array $attributes): array 399 | { 400 | $relations = []; 401 | $potential = array_diff(array_keys($attributes), $entity->getFillable()); 402 | 403 | array_walk($potential, function ($relation) use ($entity, $attributes, &$relations) { 404 | if (method_exists($entity, $relation)) { 405 | $relations[$relation] = [ 406 | 'values' => $attributes[$relation], 407 | 'class' => get_class($entity->{$relation}()), 408 | ]; 409 | } 410 | }); 411 | 412 | return $relations; 413 | } 414 | 415 | /** 416 | * Sync relationships. 417 | * 418 | * @param mixed $entity 419 | * @param array $relations 420 | * @param bool $detaching 421 | * 422 | * @return void 423 | */ 424 | protected function syncRelations($entity, array $relations, $detaching = true): void 425 | { 426 | foreach ($relations as $method => $relation) { 427 | switch ($relation['class']) { 428 | case 'Illuminate\Database\Eloquent\Relations\BelongsToMany': 429 | default: 430 | $entity->{$method}()->sync((array) $relation['values'], $detaching); 431 | break; 432 | } 433 | } 434 | } 435 | } 436 | -------------------------------------------------------------------------------- /src/Traits/Bindable.php: -------------------------------------------------------------------------------- 1 | app->bound($abstract) || $force) { 30 | $concrete = $concrete ?: $abstract; 31 | $this->app->bind($abstract, $concrete, $shared); 32 | $this->app->alias($abstract, $this->prepareRepositoryAlias($alias, $concrete)); 33 | } 34 | } 35 | 36 | /** 37 | * Prepare the repository alias. 38 | * 39 | * @param string|null $alias 40 | * @param mixed $concrete 41 | * 42 | * @return string 43 | */ 44 | protected function prepareRepositoryAlias($alias, $concrete): string 45 | { 46 | if (! $alias && ! $concrete instanceof \Closure) { 47 | $concrete = str_replace('Repositories', 'Contracts', $concrete); 48 | $alias = str_replace('{{class}}', $concrete, $this->repositoryAliasPattern); 49 | } 50 | 51 | return $alias; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Traits/Cacheable.php: -------------------------------------------------------------------------------- 1 | getRepositoryId(), 43 | $this->getModel(), 44 | $this->getCacheDriver(), 45 | $this->getCacheLifetime(), 46 | $this->relations, 47 | $this->where, 48 | $this->whereIn, 49 | $this->whereNotIn, 50 | $this->offset, 51 | $this->limit, 52 | $this->orderBy, 53 | ])); 54 | } 55 | 56 | /** 57 | * Store cache keys by mimicking cache tags. 58 | * 59 | * @param string $class 60 | * @param string $method 61 | * @param string $hash 62 | * 63 | * @return void 64 | */ 65 | protected function storeCacheKeys($class, $method, $hash): void 66 | { 67 | $keysFile = $this->getContainer('config')->get('rinvex.repository.cache.keys_file'); 68 | $cacheKeys = $this->getCacheKeys($keysFile); 69 | 70 | if (! isset($cacheKeys[$class]) || ! in_array($method.'.'.$hash, $cacheKeys[$class])) { 71 | $cacheKeys[$class][] = $method.'.'.$hash; 72 | file_put_contents($keysFile, json_encode($cacheKeys)); 73 | } 74 | } 75 | 76 | /** 77 | * Get cache keys. 78 | * 79 | * @param string $file 80 | * 81 | * @return array 82 | */ 83 | protected function getCacheKeys($file): array 84 | { 85 | if (! file_exists($file)) { 86 | file_put_contents($file, null); 87 | } 88 | 89 | return json_decode(file_get_contents($file), true) ?: []; 90 | } 91 | 92 | /** 93 | * Flush cache keys by mimicking cache tags. 94 | * 95 | * @return array 96 | */ 97 | protected function flushCacheKeys(): array 98 | { 99 | $flushedKeys = []; 100 | $calledClass = static::class; 101 | $config = $this->getContainer('config')->get('rinvex.repository.cache'); 102 | $cacheKeys = $this->getCacheKeys($config['keys_file']); 103 | 104 | if (isset($cacheKeys[$calledClass]) && is_array($cacheKeys[$calledClass])) { 105 | foreach ($cacheKeys[$calledClass] as $cacheKey) { 106 | $flushedKeys[] = $calledClass.'@'.$cacheKey; 107 | } 108 | 109 | unset($cacheKeys[$calledClass]); 110 | file_put_contents($config['keys_file'], json_encode($cacheKeys)); 111 | } 112 | 113 | return $flushedKeys; 114 | } 115 | 116 | /** 117 | * {@inheritdoc} 118 | */ 119 | public function setCacheLifetime($cacheLifetime) 120 | { 121 | $this->cacheLifetime = $cacheLifetime; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * {@inheritdoc} 128 | */ 129 | public function getCacheLifetime(): int 130 | { 131 | // Return value even if it's zero "0" (which means cache is disabled) 132 | return $this->cacheLifetime ?? $this->getContainer('config')->get('rinvex.repository.cache.lifetime'); 133 | } 134 | 135 | /** 136 | * {@inheritdoc} 137 | */ 138 | public function setCacheDriver($cacheDriver) 139 | { 140 | $this->cacheDriver = $cacheDriver; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * {@inheritdoc} 147 | */ 148 | public function getCacheDriver(): ?string 149 | { 150 | return $this->cacheDriver; 151 | } 152 | 153 | /** 154 | * {@inheritdoc} 155 | */ 156 | public function enableCacheClear($status = true) 157 | { 158 | $this->cacheClearEnabled = $status; 159 | 160 | return $this; 161 | } 162 | 163 | /** 164 | * {@inheritdoc} 165 | */ 166 | public function isCacheClearEnabled(): bool 167 | { 168 | return $this->cacheClearEnabled; 169 | } 170 | 171 | /** 172 | * {@inheritdoc} 173 | */ 174 | public function forgetCache() 175 | { 176 | if ($this->getCacheLifetime()) { 177 | // Flush cache tags 178 | if (method_exists($this->getContainer('cache')->getStore(), 'tags')) { 179 | $this->getContainer('cache')->tags($this->getRepositoryId())->flush(); 180 | } else { 181 | // Flush cache keys, then forget actual cache 182 | foreach ($this->flushCacheKeys() as $cacheKey) { 183 | $this->getContainer('cache')->forget($cacheKey); 184 | } 185 | } 186 | 187 | $this->getContainer('events')->dispatch($this->getRepositoryId().'.entity.cache.flushed', [$this]); 188 | } 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * Cache given callback. 195 | * 196 | * @param string $class 197 | * @param string $method 198 | * @param array $args 199 | * @param \Closure $closure 200 | * 201 | * @return mixed 202 | */ 203 | protected function cacheCallback($class, $method, $args, Closure $closure) 204 | { 205 | $repositoryId = $this->getRepositoryId(); 206 | $lifetime = $this->getCacheLifetime(); 207 | $hash = $this->generateCacheHash($args); 208 | $cacheKey = $class.'@'.$method.'.'.$hash; 209 | 210 | // Switch cache driver on runtime 211 | if ($driver = $this->getCacheDriver()) { 212 | $this->getContainer('cache')->setDefaultDriver($driver); 213 | } 214 | 215 | // We need cache tags, check if default driver supports it 216 | if (method_exists($this->getContainer('cache')->getStore(), 'tags')) { 217 | $result = $lifetime === -1 218 | ? $this->getContainer('cache')->tags($repositoryId)->rememberForever($cacheKey, $closure) 219 | : $this->getContainer('cache')->tags($repositoryId)->remember($cacheKey, $lifetime, $closure); 220 | 221 | // We're done, let's clean up! 222 | $this->resetRepository(); 223 | 224 | return $result; 225 | } 226 | 227 | // Default cache driver doesn't support tags, let's do it manually 228 | $this->storeCacheKeys($class, $method, $hash); 229 | 230 | $result = $lifetime === -1 231 | ? $this->getContainer('cache')->rememberForever($cacheKey, $closure) 232 | : $this->getContainer('cache')->remember($cacheKey, $lifetime, $closure); 233 | 234 | // We're done, let's clean up! 235 | $this->resetCachedRepository(); 236 | 237 | return $result; 238 | } 239 | 240 | /** 241 | * Reset cached repository to its defaults. 242 | * 243 | * @return $this 244 | */ 245 | protected function resetCachedRepository() 246 | { 247 | $this->resetRepository(); 248 | 249 | $this->cacheLifetime = null; 250 | $this->cacheDriver = null; 251 | 252 | return $this; 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Traits/Criteriable.php: -------------------------------------------------------------------------------- 1 | implementsInterface(CriterionContract::class)) { 78 | throw CriterionException::classNotImplementContract($class); 79 | } 80 | 81 | // If arguments is an associative array we can assume their order and parameter existence 82 | if (Arr::isAssoc($arguments)) { 83 | $parameters = array_column($reflection->getConstructor()->getParameters(), 'name'); 84 | 85 | $arguments = array_filter(array_map(function ($parameter) use ($arguments) { 86 | return $arguments[$parameter] ?? null; 87 | }, $parameters)); 88 | } 89 | 90 | return $reflection->newInstanceArgs($arguments); 91 | } 92 | 93 | /** 94 | * Return class and arguments from passed array criterion. 95 | * Extracting class and arguments from array. 96 | * 97 | * @param array $criterion 98 | * 99 | * @throws CriterionException 100 | * 101 | * @return array 102 | */ 103 | protected function extractCriterionClassAndArgs(array $criterion): array 104 | { 105 | if (count($criterion) > 2 || empty($criterion)) { 106 | throw CriterionException::wrongArraySignature($criterion); 107 | } 108 | 109 | // If an array is assoc we assume that the key is a class and value is an arguments 110 | if (Arr::isAssoc($criterion)) { 111 | $criterion = [array_keys($criterion)[0], array_values($criterion)[0]]; 112 | } elseif (count($criterion) === 1) { 113 | // If an array is not assoc but count is one, we can assume there is a class without arguments. 114 | // Like when a string passed as criterion 115 | array_push($criterion, []); 116 | } 117 | 118 | return $criterion; 119 | } 120 | 121 | /** 122 | * Add criterion to the specific list. 123 | * low-level implementation of adding criterion to the list. 124 | * 125 | * @param Closure|CriterionContract|array|string $criterion 126 | * @param string $list 127 | * 128 | * @throws CriterionException 129 | * @throws RepositoryException 130 | * 131 | * @return $this 132 | */ 133 | protected function addCriterion($criterion, $list) 134 | { 135 | if (! property_exists($this, $list)) { 136 | throw RepositoryException::listNotFound($list, $this); 137 | } 138 | 139 | if (! $criterion instanceof Closure && 140 | ! $criterion instanceof CriterionContract && 141 | ! is_string($criterion) && 142 | ! is_array($criterion) 143 | ) { 144 | throw CriterionException::wrongCriterionType($criterion); 145 | } 146 | 147 | //If criterion is a string we will assume it is a class name without arguments 148 | //and we need to normalize signature for instantiation try 149 | if (is_string($criterion)) { 150 | $criterion = [$criterion, []]; 151 | } 152 | 153 | //If the criterion is an array we will assume it is an array of class name with arguments 154 | //and try to instantiate this 155 | if (is_array($criterion)) { 156 | $criterion = call_user_func_array([$this, 'instantiateCriterion'], $this->extractCriterionClassAndArgs($criterion)); 157 | } 158 | 159 | $this->{$list}[$this->getCriterionName($criterion)] = $criterion; 160 | 161 | return $this; 162 | } 163 | 164 | /** 165 | * Add criteria to the specific list 166 | * low-level implementation of adding criteria to the list. 167 | * 168 | * @param array $criteria 169 | * @param $list 170 | */ 171 | protected function addCriteria(array $criteria, $list) 172 | { 173 | array_walk($criteria, function ($value, $key) use ($list) { 174 | $criterion = is_string($key) ? [$key, $value] : $value; 175 | $this->addCriterion($criterion, $list); 176 | }); 177 | } 178 | 179 | /** 180 | * Push criterion to the criteria list. 181 | * 182 | * @param CriterionContract|Closure|array|string $criterion 183 | * 184 | * @return $this 185 | */ 186 | public function pushCriterion($criterion) 187 | { 188 | $this->addCriterion($criterion, 'criteria'); 189 | 190 | return $this; 191 | } 192 | 193 | /** 194 | * Remove provided criterion from criteria list. 195 | * 196 | * @param CriterionContract|Closure|string $criterion 197 | * 198 | * @return $this 199 | */ 200 | public function removeCriterion($criterion) 201 | { 202 | unset($this->criteria[$this->getCriterionName($criterion)]); 203 | 204 | return $this; 205 | } 206 | 207 | /** 208 | * Remove provided criteria from criteria list. 209 | * 210 | * @param array $criteria 211 | * 212 | * @return RepositoryContract 213 | */ 214 | public function removeCriteria(array $criteria) 215 | { 216 | array_walk($criteria, function ($criterion) { 217 | $this->removeCriterion($criterion); 218 | }); 219 | 220 | return $this; 221 | } 222 | 223 | /** 224 | * Push array of criteria to the criteria list. 225 | * 226 | * @param array $criteria 227 | * 228 | * @return $this 229 | */ 230 | public function pushCriteria(array $criteria) 231 | { 232 | $this->addCriteria($criteria, 'criteria'); 233 | 234 | return $this; 235 | } 236 | 237 | /** 238 | * Flush criteria list. 239 | * We can flush criteria only when they is not skipped. 240 | * 241 | * @return $this 242 | */ 243 | public function flushCriteria() 244 | { 245 | if (! $this->skipCriteria) { 246 | $this->criteria = []; 247 | } 248 | 249 | return $this; 250 | } 251 | 252 | /** 253 | * Set default criteria list. 254 | * 255 | * @param array $criteria 256 | * 257 | * @return $this 258 | */ 259 | public function setDefaultCriteria(array $criteria) 260 | { 261 | $this->addCriteria($criteria, 'defaultCriteria'); 262 | 263 | return $this; 264 | } 265 | 266 | /** 267 | * Return default criteria list. 268 | * 269 | * @return array 270 | */ 271 | public function getDefaultCriteria(): array 272 | { 273 | return $this->defaultCriteria; 274 | } 275 | 276 | /** 277 | * Return current list of criteria. 278 | * 279 | * @return array 280 | */ 281 | public function getCriteria(): array 282 | { 283 | if ($this->skipCriteria) { 284 | return []; 285 | } 286 | 287 | return $this->skipDefaultCriteria ? $this->criteria : array_merge($this->getDefaultCriteria(), $this->criteria); 288 | } 289 | 290 | /** 291 | * Set skipCriteria flag. 292 | * 293 | * @param bool|true $flag 294 | * 295 | * @return $this 296 | */ 297 | public function skipCriteria($flag = true) 298 | { 299 | $this->skipCriteria = $flag; 300 | 301 | return $this; 302 | } 303 | 304 | /** 305 | * Set skipDefaultCriteria flag. 306 | * 307 | * @param bool|true $flag 308 | * 309 | * @return $this 310 | */ 311 | public function skipDefaultCriteria($flag = true) 312 | { 313 | $this->skipDefaultCriteria = $flag; 314 | 315 | return $this; 316 | } 317 | 318 | /** 319 | * Check if a given criterion name now in the criteria list. 320 | * 321 | * @param CriterionContract|Closure|string $criterion 322 | * 323 | * @return bool 324 | */ 325 | public function hasCriterion($criterion): bool 326 | { 327 | return isset($this->getCriteria()[$this->getCriterionName($criterion)]); 328 | } 329 | 330 | /** 331 | * Return criterion object or closure from criteria list by name. 332 | * 333 | * @param $criterion 334 | * 335 | * @return CriterionContract|Closure|null 336 | */ 337 | public function getCriterion($criterion) 338 | { 339 | if ($this->hasCriterion($criterion)) { 340 | return $this->getCriteria()[$this->getCriterionName($criterion)]; 341 | } 342 | } 343 | 344 | /** 345 | * Apply criteria list to the given query. 346 | * 347 | * @param $query 348 | * @param $repository 349 | * 350 | * @return mixed 351 | */ 352 | public function applyCriteria($query, $repository) 353 | { 354 | foreach ($this->getCriteria() as $criterion) { 355 | if ($criterion instanceof CriterionContract) { 356 | $query = $criterion->apply($query, $repository); 357 | } elseif ($criterion instanceof Closure) { 358 | $query = $criterion($query, $repository); 359 | } 360 | } 361 | 362 | return $query; 363 | } 364 | } 365 | --------------------------------------------------------------------------------