├── .php-cs-fixer.php ├── .phpunit.cache └── test-results ├── .styleci.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── composer.json ├── config └── review-rating.php ├── database └── migrations │ └── create_reviews_table.php.stub ├── psalm.xml └── src ├── Events └── ReviewCreatedEvent.php ├── Exceptions ├── InvalidDate.php └── InvalidReviewModel.php ├── Models └── Review.php ├── ReviewRatingServiceProvider.php └── Traits └── HasReviewRating.php /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | notPath('bootstrap/*') 5 | ->notPath('storage/*') 6 | ->notPath('storage/*') 7 | ->notPath('resources/view/mail/*') 8 | ->in([ 9 | __DIR__ . '/src', 10 | __DIR__ . '/tests', 11 | ]) 12 | ->name('*.php') 13 | ->notName('*.blade.php') 14 | ->ignoreDotFiles(true) 15 | ->ignoreVCS(true); 16 | 17 | return PhpCsFixer\Config::create() 18 | ->setRules([ 19 | '@PSR2' => true, 20 | 'array_syntax' => ['syntax' => 'short'], 21 | 'ordered_imports' => ['sortAlgorithm' => 'alpha'], 22 | 'no_unused_imports' => true, 23 | 'not_operator_with_successor_space' => true, 24 | 'trailing_comma_in_multiline_array' => true, 25 | 'phpdoc_scalar' => true, 26 | 'unary_operator_spaces' => true, 27 | 'binary_operator_spaces' => true, 28 | 'blank_line_before_statement' => [ 29 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 30 | ], 31 | 'phpdoc_single_line_var_spacing' => true, 32 | 'phpdoc_var_without_name' => true, 33 | 'method_argument_space' => [ 34 | 'on_multiline' => 'ensure_fully_multiline', 35 | 'keep_multiple_spaces_after_comma' => true, 36 | ] 37 | ]) 38 | ->setFinder($finder); 39 | -------------------------------------------------------------------------------- /.phpunit.cache/test-results: -------------------------------------------------------------------------------- 1 | {"version":1,"defects":[],"times":{"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_create_review_without_rating_and_title":0.009,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_create_review_with_rating":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_create_review_with_rating_and_title":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_handle_getting_a_review_when_there_are_none_set":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_the_latest_review":0.002,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_all_reviews":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_use_a_different_review_model":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_use_a_custom_name_for_the_relationship_id_column":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_uses_the_default_relationship_id_column_when_configuration_value_is_no_present":0.001,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_number_of_reviews":0.002,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_throws_error_when_from_date_is_later_than_to_date":0.002,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_number_of_reviews_over_a_period":0.002,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_get_scoped_reviews":0.005,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_number_of_ratings":0.002,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_return_number_of_ratings_over_a_period":0.004,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_get_scoped_reviews_with_rating":0.004,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_get_average_rating":0.003,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_get_average_rating_over_a_period":0.004,"Digikraaft\\ReviewRating\\Tests\\HasReviewRatingTest::it_can_check_if_user_has_reviewed":0.001}} -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | 3 | disabled: 4 | - single_class_element_per_statement 5 | - self_accessor 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.0.0 2023-03-04 2 | - Added support for Laravel 10 3 | 4 | ## 2.3.1 2022-10-26 5 | - Fixed spelling in readme by 6 | - Fix failing test and use the instance of the created review 7 | 8 | ## 2.3.0 2022-02-17 9 | - Add support for Laravel 9 10 | - Update test for Laravel 9 11 | - Pretty 12 | 13 | ## 2.2.0 - 2022-01-25 14 | - Fixed support for PHP 8.* 15 | - Fixed averageRating 16 | 17 | ## 2.1.0 - 2021-11-02 18 | - Support for PHP 8 19 | 20 | ## 2.0.1 - 2021-07-20 21 | - Bug fixes 22 | - Author Model Added 23 | 24 | ## 2.0.0 - 2020-10-20 25 | - Support for Laravel 8 26 | ## 1.0.0 - 2020-08-01 27 | - initial release. 28 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | Please read and understand the contribution guide before creating an issue or pull request. 6 | 7 | ## Etiquette 8 | 9 | This project is open source, and as such, the maintainers give their free time to build and maintain the source code 10 | held within. They make the code freely available in the hope that it will be of use to other developers. It would be 11 | extremely unfair for them to suffer abuse or anger for their hard work. 12 | 13 | Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the 14 | world that developers are civilized and selfless people. 15 | 16 | It's the duty of the maintainer to ensure that all submissions to the project are of sufficient 17 | quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used. 18 | 19 | ## Viability 20 | 21 | When requesting or submitting new features, first consider whether it might be useful to others. Open 22 | source projects are used by many developers, who may have entirely different needs to your own. Think about 23 | whether or not your feature is likely to be used by other users of the project. 24 | 25 | ## Procedure 26 | 27 | Before filing an issue: 28 | 29 | - Attempt to replicate the problem, to ensure that it wasn't a coincidental incident. 30 | - Check to make sure your feature suggestion isn't already present within the project. 31 | - Check the pull requests tab to ensure that the bug doesn't have a fix in progress. 32 | - Check the pull requests tab to ensure that the feature isn't already in progress. 33 | 34 | Before submitting a pull request: 35 | 36 | - Check the codebase to ensure that your feature doesn't already exist. 37 | - Check the pull requests to ensure that another person hasn't already submitted the feature or fix. 38 | 39 | ## Requirements 40 | 41 | If the project maintainer has any additional requirements, you will find them listed here. 42 | 43 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer). 44 | 45 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 46 | 47 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 48 | 49 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 50 | 51 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 52 | 53 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting. 54 | 55 | **Happy coding**! 56 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Digikraaft 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 | # Add Review and Rating Feature to your Laravel application 2 | ![tests](https://github.com/digikraaft/laravel-review-rating/workflows/tests/badge.svg?branch=master) 3 | [![Build Status](https://scrutinizer-ci.com/g/digikraaft/laravel-review-rating/badges/build.png?b=master)](https://scrutinizer-ci.com/g/digikraaft/laravel-model-suspension/build-status/master) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/digikraaft/laravel-review-rating/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/digikraaft/laravel-model-suspension/?branch=master) 5 | [![Code Intelligence Status](https://scrutinizer-ci.com/g/digikraaft/laravel-review-rating/badges/code-intelligence.svg?b=master)](https://scrutinizer-ci.com/code-intelligence) 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](https://opensource.org/licenses/MIT) 7 | 8 | ## Review and Rating System for Laravel 9 | This package provides a simple review and rating system for Laravel. It supports 10 | Laravel 5.8 an up. Here is a quick demonstration of how it can be used: 11 | 12 | ```php 13 | //create a review 14 | $author = User::find(1); 15 | $review = "Awesome package! I highly recommend it!!"; 16 | 17 | $model->review($review, $author); 18 | 19 | //write a review and include a rating 20 | $model->review($review, $author, 5); 21 | 22 | //write a review and include a rating and a title 23 | $model->review($review, $author, 5, "Lovely packages"); 24 | 25 | //get the last review 26 | $model->latestReview(); //returns an instance of \Digikraaft\ReviewRating\Review 27 | 28 | //get the review content of the last review 29 | $model->latestReview()->review; //returns 'Awesome package! I highly recommend it!!' 30 | 31 | //get the rating of the last review 32 | $model->latestReview()->rating; //return 5 33 | 34 | //get the title of the last review 35 | $model->latestReview()->title; //returns 'Lovely packages' 36 | ``` 37 | 38 | ## Installation 39 | 40 | You can install the package via composer: 41 | 42 | ```bash 43 | composer require digikraaft/laravel-review-rating 44 | ``` 45 | You must publish the migration with: 46 | ```bash 47 | php artisan vendor:publish --provider="Digikraaft\ReviewRating\ReviewRatingServiceProvider" --tag="migrations" 48 | ``` 49 | Run the migration to publish the `reviews` table with: 50 | ```bash 51 | php artisan migrate 52 | ``` 53 | You can optionally publish the config-file with: 54 | ```bash 55 | php artisan vendor:publish --provider="Digikraaft\ReviewRating\ReviewRatingServiceProvider" --tag="config" 56 | ``` 57 | The content of the file that will be published to `config/review-rating.php`: 58 | ```php 59 | return [ 60 | /* 61 | * The class name of the review model that holds all reviews. 62 | * 63 | * The model must be or extend `Digikraaft\ReviewRating\Review`. 64 | */ 65 | 'review_model' => Digikraaft\ReviewRating\Models\Review::class, 66 | 67 | /* 68 | * The name of the column which holds the ID of the model related to the reviews. 69 | * 70 | * Only change this value if you have set a different name in the migration for the reviews table. 71 | */ 72 | 'model_primary_key_attribute' => 'model_id', 73 | 74 | ]; 75 | ``` 76 | ## Usage 77 | Add the `HasReviewRating` trait to the model: 78 | ```php 79 | use Digikraaft\ReviewRating\Traits\HasReviewRating; 80 | use Illuminate\Database\Eloquent\Model; 81 | 82 | class EloquentModel extends Model 83 | { 84 | use HasReviewRating; 85 | } 86 | ``` 87 | 88 | ### Create a review 89 | To create a review, use the `review` function of the trait. 90 | Like this: 91 | ```php 92 | $author = User::find(1); 93 | $review = "Awesome package! I highly recommend it!!"; 94 | 95 | $model->review($review, $author); 96 | ``` 97 | The first argument is the content of the review while the second argument is the author. 98 | This can be any Eloquent model. 99 | 100 | To create a review with rating, pass in the rating value as the third argument of 101 | the `review` function. Valid values are `int`s and `float`s: 102 | ```php 103 | $author = User::find(1); 104 | $review = "Awesome package! I highly recommend it!!"; 105 | 106 | $model->review($review, $author, 5); 107 | ``` 108 | 109 | To create a review with rating and title, add the title as the fourth argument 110 | of the `review` function: 111 | ```php 112 | $author = User::find(1); 113 | $review = "Awesome package! I highly recommend it!!"; 114 | 115 | $model->review($review, $author, 5, "Lovely packages"); 116 | ``` 117 | 118 | You can also check if user has reviewed the model by using the `hasReviewed` function: 119 | ```php 120 | if ($model->hasReviewed(auth()->user())) { 121 | // user has reviewed the model 122 | } 123 | ``` 124 | 125 | ### Retrieving reviews 126 | You can get the last review like this: 127 | ```php 128 | $model->latestReview(); //returns the latest instance of Digikraaft\ReviewRating\Review 129 | ``` 130 | The content of the review can be gotten like this: 131 | ```php 132 | $model->latestReview()->review; 133 | ``` 134 | To get the rating for the review, do this: 135 | ```php 136 | $model->latestReview()->rating; 137 | ``` 138 | To get the title of the review: 139 | ```php 140 | $model->latestReview()->title; 141 | ``` 142 | All reviews can be retrieved like this: 143 | ```php 144 | $model->reviews; 145 | ``` 146 | To access each review from the reviews retrieved, do this: 147 | ```php 148 | $reviews = $model->reviews; 149 | 150 | foreach($reviews as $review){ 151 | echo $review->review . "
"; 152 | } 153 | ``` 154 | The `allReviews` scope can be used to retrieve all the reviews for all instances of a model: 155 | ```php 156 | $allReviews = EloquentModel::allReviews(); 157 | ``` 158 | ### Retrieving basic Review Stats 159 | You can get the number of reviews a model has: 160 | ```php 161 | $model->numberOfReviews(); 162 | ``` 163 | To get the number of reviews a model has received over a period, 164 | pass in a `Carbon` formatted `$from` and `$to` dates as the first and second 165 | arguments respectively: 166 | ```php 167 | //get the number of reviews a model has received over the last month 168 | $from = now()->subMonth(); 169 | $to = now(); 170 | 171 | $model->numberOfReviews($from, $to); 172 | ``` 173 | Note that an `InvalidDate` exception will be thrown if the `$from` date is later than the `$to` 174 | 175 | You can get the number of ratings a model has: 176 | ```php 177 | $model->numberOfRatings(); 178 | ``` 179 | To get the number of ratings a model has received over a period, 180 | pass in a `Carbon` formatted `$from` and `$to` dates as the first and second 181 | arguments respectively: 182 | ```php 183 | //get the number of reviews a model has received over the last month 184 | $from = now()->subMonth(); 185 | $to = now(); 186 | 187 | $model->numberOfRatings($from, $to); 188 | ``` 189 | To get the average rating a model has received: 190 | ```php 191 | $model->averageRating(); 192 | ``` 193 | The average rating that is returned is by default not rounded. 194 | If you would like to `round` the returned result, pass an integer value of the 195 | decimal place you want it rounded to. 196 | ```php 197 | //round up to 2 decimal places 198 | $model->averageRating(2); 199 | ``` 200 | To get the average rating a model has received over a period, 201 | pass in a `Carbon` formatted `$from` and `$to` dates as the first and second 202 | arguments respectively: 203 | ```php 204 | //get the average rating a model has received over the last month, rounded to 2 decimal places: 205 | $from = now()->subMonth(); 206 | $to = now(); 207 | 208 | $model->averageRating(2, $from, $to); 209 | ``` 210 | The `withRatings` scope can be used to retrieve all the reviews that have a rating for all instances of a model: 211 | ```php 212 | $allReviewsWithRating = EloquentModel::withRatings(); 213 | ``` 214 | 215 | ### Check if model has review 216 | You can check if a model has at least one review: 217 | ```php 218 | $model->hasReview(); 219 | ``` 220 | ### Check if model has rating 221 | You can check if a model has at least one rating: 222 | ```php 223 | $model->hasRating(); 224 | ``` 225 | 226 | ### Events 227 | The `Digikraaft\ReviewRating\Events\ReviewCreatedEvent` event will be dispatched when 228 | a review has been created. You can listen to this event and take necessary actions. 229 | An instance of the review will be passed to the event class and can be accessed for use: 230 | ```php 231 | namespace Digikraaft\ReviewRating\Events; 232 | 233 | use Digikraaft\ReviewRating\Models\Review; 234 | use Illuminate\Database\Eloquent\Model; 235 | 236 | class ReviewCreatedEvent 237 | { 238 | /** @var \Digikraaft\ReviewRating\Models\Review */ 239 | public Review $review; 240 | 241 | public function __construct(Review $review) 242 | { 243 | $this->review = $review; 244 | } 245 | } 246 | ``` 247 | 248 | ### Custom model and migration 249 | You can change the model used by specifying a different class name in the 250 | `review_model` key of the `review-rating` config file. 251 | 252 | You can also change the column name used in the `reviews` table 253 | (default is `model_id`) when using a custom migration. If this is the case, 254 | also change the `model_primary_key_attribute` key of the `review-rating` config file. 255 | 256 | ## Testing 257 | Use the command below to run your tests: 258 | ```bash 259 | composer test 260 | ``` 261 | 262 | ## More Good Stuff 263 | Check [here](https://github.com/digikraaft) for more awesome free stuff! 264 | 265 | ## Changelog 266 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 267 | 268 | ## Contributing 269 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 270 | 271 | ## Security 272 | If you discover any security related issues, please email hello@digikraaft.ng instead of using the issue tracker. 273 | 274 | ## Credits 275 | - [Tim Oladoyinbo](https://github.com/timoladoyinbo) 276 | - [All Contributors](../../contributors) 277 | 278 | ## License 279 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 280 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "digikraaft/laravel-review-rating", 3 | "description": "Review & Rating system for Laravel", 4 | "keywords": [ 5 | "digikraaft", 6 | "reviews", 7 | "ratings", 8 | "laravel" 9 | ], 10 | "homepage": "https://github.com/digikraaft/laravel-review-rating", 11 | "license": "MIT", 12 | "authors": [ 13 | { 14 | "name": "Tim Oladoyinbo", 15 | "email": "hello@digikraaft.ng", 16 | "homepage": "https://digikraaft.com", 17 | "role": "Developer" 18 | } 19 | ], 20 | "require": { 21 | "php": "^8.1", 22 | "laravel/framework": "^10.0|^11.0" 23 | }, 24 | "require-dev": { 25 | "orchestra/testbench": "^8.0|^9.0", 26 | "phpunit/phpunit": "^10.0|^11.0", 27 | "spatie/test-time": "^1.3" 28 | }, 29 | "autoload": { 30 | "psr-4": { 31 | "Digikraaft\\ReviewRating\\": "src" 32 | } 33 | }, 34 | "autoload-dev": { 35 | "psr-4": { 36 | "Digikraaft\\ReviewRating\\Tests\\": "tests" 37 | } 38 | }, 39 | "scripts": { 40 | "test": "vendor/bin/phpunit", 41 | "test-coverage": "vendor/bin/phpunit --coverage-html coverage" 42 | }, 43 | "config": { 44 | "sort-packages": true 45 | }, 46 | "extra": { 47 | "laravel": { 48 | "providers": [ 49 | "Digikraaft\\ReviewRating\\ReviewRatingServiceProvider" 50 | ] 51 | } 52 | }, 53 | "minimum-stability": "dev", 54 | "prefer-stable": true 55 | } 56 | -------------------------------------------------------------------------------- /config/review-rating.php: -------------------------------------------------------------------------------- 1 | Digikraaft\ReviewRating\Models\Review::class, 10 | 11 | /* 12 | * The name of the column which holds the ID of the model related to the reviews. 13 | * 14 | * Only change this value if you have set a different name in the migration for the reviews table. 15 | */ 16 | 'model_primary_key_attribute' => 'model_id', 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /database/migrations/create_reviews_table.php.stub: -------------------------------------------------------------------------------- 1 | increments('id'); 18 | $table->string('title')->nullable(); 19 | $table->text('review'); 20 | $table->integer('rating')->nullable(); 21 | $table->morphs('model'); 22 | $table->morphs('author'); 23 | $table->timestamps(); 24 | $table->softDeletes(); 25 | }); 26 | } 27 | 28 | /** 29 | * Reverse the migrations. 30 | * 31 | * @return void 32 | */ 33 | public function down() 34 | { 35 | Schema::dropIfExists('reviews'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Events/ReviewCreatedEvent.php: -------------------------------------------------------------------------------- 1 | review = $review; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidDate.php: -------------------------------------------------------------------------------- 1 | morphTo(); 16 | } 17 | 18 | public function author(): MorphTo 19 | { 20 | return $this->morphTo(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/ReviewRatingServiceProvider.php: -------------------------------------------------------------------------------- 1 | registerPublishables(); 14 | } 15 | 16 | public function register() 17 | { 18 | $this->mergeConfigFrom(__DIR__ . '/../config/review-rating.php', 'review-rating'); 19 | } 20 | 21 | protected function registerPublishables(): void 22 | { 23 | if ($this->app->runningInConsole()) { 24 | $this->loadMigrationsFrom(__DIR__.'/../database/migrations/'); 25 | } 26 | 27 | if (! class_exists('CreateReviewsTable')) { 28 | $timestamp = date('Y_m_d_His', time()); 29 | 30 | $this->publishes([ 31 | __DIR__ . '/../database/migrations/create_reviews_table.php.stub' => database_path('migrations/'.$timestamp.'_create_reviews_table.php'), 32 | ], 'migrations'); 33 | } 34 | 35 | $this->publishes([ 36 | __DIR__ . '/../config/review-rating.php' => config_path('review-rating.php'), 37 | ], 'config'); 38 | 39 | $this->guardAgainstInvalidReviewModel(); 40 | } 41 | 42 | public function guardAgainstInvalidReviewModel() 43 | { 44 | $modelClassName = config('review-rating.review_model'); 45 | 46 | if (! is_a($modelClassName, Review::class, true)) { 47 | throw InvalidReviewModel::create($modelClassName); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Traits/HasReviewRating.php: -------------------------------------------------------------------------------- 1 | morphMany($this->getReviewModelClassName(), 'model', 'model_type', $this->getReviewKeyColumnName()) 20 | ->latest('id'); 21 | } 22 | 23 | public function latestReview() 24 | { 25 | return $this->reviews()->first(); 26 | } 27 | 28 | 29 | public function review(string $review, Model $author, ?float $rating = null, ?string $title = null) : self 30 | { 31 | return $this->createReview($review, $author, $rating, $title); 32 | } 33 | 34 | private function createReview($review, $author, $rating, $title) 35 | { 36 | $keyName = $author->getKeyName(); 37 | $createdReview = $this->reviews()->create([ 38 | 'review' => $review, 39 | 'author_id' => $author->$keyName, 40 | 'author_type' => $author->getMorphClass(), 41 | 'rating' => $rating, 42 | 'title' => $title, 43 | ]); 44 | 45 | event(new ReviewCreatedEvent($createdReview)); 46 | 47 | return $this; 48 | } 49 | 50 | public function hasReview(): bool 51 | { 52 | return $this->reviews()->count() > 0; 53 | } 54 | 55 | public function hasReviewed(Model $author): bool 56 | { 57 | $keyName = $author->getKeyName(); 58 | 59 | return $this->reviews() 60 | ->where('author_id', $author->$keyName) 61 | ->where('author_type', $author->getMorphClass()) 62 | ->count() > 0; 63 | } 64 | 65 | public function hasRating(): bool 66 | { 67 | return $this->reviews() 68 | ->whereNotNull('rating') 69 | ->count() > 0; 70 | } 71 | 72 | /** 73 | * @param Carbon|null $from 74 | * @param Carbon|null $to 75 | * @return int 76 | * @throws InvalidDate 77 | */ 78 | public function numberOfReviews(?Carbon $from = null, ?Carbon $to = null): int 79 | { 80 | if (! $from && ! $to) { 81 | return $this->reviews()->count(); 82 | } 83 | 84 | if ($from->greaterThan($to)) { 85 | throw InvalidDate::from(); 86 | } 87 | 88 | return $this->reviews() 89 | ->whereBetween( 90 | 'created_at', 91 | [$from->toDateTimeString(), $to->toDateTimeString()] 92 | )->count(); 93 | } 94 | 95 | /** 96 | * @param Carbon|null $from 97 | * @param Carbon|null $to 98 | * @return int 99 | * @throws InvalidDate 100 | */ 101 | public function numberOfRatings(?Carbon $from = null, ?Carbon $to = null): int 102 | { 103 | if (! $from && ! $to) { 104 | return $this->reviews() 105 | ->whereNotNull('rating') 106 | ->count(); 107 | } 108 | 109 | if ($from->greaterThan($to)) { 110 | throw InvalidDate::from(); 111 | } 112 | 113 | return $this->reviews() 114 | ->whereNotNull('rating') 115 | ->whereBetween( 116 | 'created_at', 117 | [$from->toDateTimeString(), $to->toDateTimeString()] 118 | )->count(); 119 | } 120 | 121 | public function averageRating(?int $round = 2, ?Carbon $from = null, ?Carbon $to = null): ?float 122 | { 123 | return $this->reviews() 124 | ->when($from && $to, function ($query) use ($from, $to) { 125 | $query->whereBetween('created_at', [ 126 | $from->toDateTimeString(), 127 | $to->toDateTimeString(), 128 | ]); 129 | }) 130 | ->when($round, function ($query) use ($round) { 131 | $query->selectRaw("ROUND(AVG(rating), $round) as rating"); 132 | }, function ($query) { 133 | $query->selectRaw("AVG(rating) as rating"); 134 | }) 135 | ->value('rating'); 136 | } 137 | 138 | protected function getReviewTableName(): string 139 | { 140 | $modelClass = $this->getReviewModelClassName(); 141 | 142 | return (new $modelClass)->getTable(); 143 | } 144 | 145 | protected function getReviewKeyColumnName(): string 146 | { 147 | return config('review-rating.model_primary_key_attribute') ?? 'model_id'; 148 | } 149 | 150 | protected function getReviewModelClassName(): string 151 | { 152 | return config('review-rating.review_model'); 153 | } 154 | 155 | protected function getReviewModelType(): string 156 | { 157 | return array_search(static::class, Relation::morphMap()) ?: static::class; 158 | } 159 | 160 | public function scopeAllReviews(Builder $builder) 161 | { 162 | $builder 163 | ->whereHas( 164 | 'reviews', 165 | function (Builder $query) { 166 | $query 167 | ->whereIn( 168 | 'id', 169 | function (QueryBuilder $query) { 170 | $query 171 | ->select(DB::raw('max(id)')) 172 | ->from($this->getReviewTableName()) 173 | ->where('model_type', $this->getReviewModelType()) 174 | ->whereColumn($this->getReviewKeyColumnName(), $this->getQualifiedKeyName()); 175 | } 176 | ); 177 | } 178 | ); 179 | } 180 | 181 | public function scopeWithRatings(Builder $builder) 182 | { 183 | $builder 184 | ->whereHas( 185 | 'reviews', 186 | function (Builder $query) { 187 | $query 188 | ->whereIn( 189 | 'id', 190 | function (QueryBuilder $query) { 191 | $query 192 | ->select(DB::raw('max(id)')) 193 | ->from($this->getReviewTableName()) 194 | ->where('model_type', $this->getReviewModelType()) 195 | ->whereColumn($this->getReviewKeyColumnName(), $this->getQualifiedKeyName()); 196 | } 197 | )->whereNotNull('rating'); 198 | } 199 | ); 200 | } 201 | } 202 | --------------------------------------------------------------------------------