├── .gitignore ├── .github └── FUNDING.yml ├── src ├── Events │ ├── CommentCreated.php │ ├── CommentDeleted.php │ └── CommentUpdated.php ├── routes.php ├── CommentController.php ├── CommentControllerInterface.php ├── Commenter.php ├── Commentable.php ├── CommentPolicy.php ├── WebCommentController.php ├── Comment.php ├── ServiceProvider.php └── CommentService.php ├── resources ├── lang │ ├── ar │ │ └── comments.php │ ├── en │ │ └── comments.php │ └── ru │ │ └── comments.php └── views │ ├── _form.blade.php │ ├── components │ └── comments.blade.php │ └── _comment.blade.php ├── LICENSE.md ├── composer.json ├── migrations └── 2018_06_30_113500_create_comments_table.php ├── config └── comments.php ├── UPGRADE.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.lock -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: laravelista 4 | -------------------------------------------------------------------------------- /src/Events/CommentCreated.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Events/CommentDeleted.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Events/CommentUpdated.php: -------------------------------------------------------------------------------- 1 | comment = $comment; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | name('comments.store'); 7 | Route::delete('comments/{comment}', Config::get('comments.controller') . '@destroy')->name('comments.destroy'); 8 | Route::put('comments/{comment}', Config::get('comments.controller') . '@update')->name('comments.update'); 9 | Route::post('comments/{comment}', Config::get('comments.controller') . '@reply')->name('comments.reply'); -------------------------------------------------------------------------------- /src/CommentController.php: -------------------------------------------------------------------------------- 1 | middleware('web'); 14 | 15 | if (Config::get('comments.guest_commenting') == true) { 16 | $this->middleware('auth')->except('store'); 17 | $this->middleware(ProtectAgainstSpam::class)->only('store'); 18 | } else { 19 | $this->middleware('auth'); 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /src/CommentControllerInterface.php: -------------------------------------------------------------------------------- 1 | morphMany(Config::get('comments.model'), 'commenter'); 19 | } 20 | 21 | /** 22 | * Returns only approved comments that this user has made. 23 | */ 24 | public function approvedComments() 25 | { 26 | return $this->morphMany(Config::get('comments.model'), 'commenter')->where('approved', true); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/lang/ar/comments.php: -------------------------------------------------------------------------------- 1 | 'لا توجد تعليقات حتي الآن.', 5 | 'authentication_required' => 'تسجيل الدخول مطلوب', 6 | 'you_must_login_to_post_a_comment' => 'يجب عليك تسجيل الدخول لإضافة تعليق.', 7 | 'log_in' => 'تسجيل الدخول', 8 | 'reply' => 'رد', 9 | 'edit' => 'تعديل', 10 | 'delete' => 'حذف', 11 | 'edit_comment' => 'تعديل التعليق', 12 | 'update_your_message_here' => 'حدّث تعليقك:', 13 | 'update' => 'تحديث', 14 | 'cancel' => 'إلغاء', 15 | 'reply_to_comment' => 'الرد علي التعليق', 16 | 'markdown_cheatsheet' => 'Markdown أوامر.', 17 | 'submit' => 'إرسال', 18 | 'your_message_is_required' => 'محتوي التعليق مطلوب.', 19 | 'enter_your_message_here' => 'أدخل تعليقك:', 20 | 'enter_your_email_here' => 'بريدك الإلكتروني:', 21 | 'enter_your_name_here' => 'اسمك:', 22 | ]; -------------------------------------------------------------------------------- /resources/lang/en/comments.php: -------------------------------------------------------------------------------- 1 | 'There are no comments yet.', 5 | 'authentication_required' => 'Authentication required', 6 | 'you_must_login_to_post_a_comment' => 'You must log in to post a comment.', 7 | 'log_in' => 'Log in', 8 | 'reply' => 'Reply', 9 | 'edit' => 'Edit', 10 | 'delete' => 'Delete', 11 | 'edit_comment' => 'Edit comment', 12 | 'update_your_message_here' => 'Update your message here:', 13 | 'update' => 'Update', 14 | 'cancel' => 'Cancel', 15 | 'reply_to_comment' => 'Reply to comment', 16 | 'markdown_cheatsheet' => 'Markdown cheatsheet.', 17 | 'submit' => 'Submit', 18 | 'your_message_is_required' => 'Your message is required.', 19 | 'enter_your_message_here' => 'Enter your message here:', 20 | 'enter_your_email_here' => 'Enter your email here:', 21 | 'enter_your_name_here' => 'Enter your name here:', 22 | ]; -------------------------------------------------------------------------------- /resources/lang/ru/comments.php: -------------------------------------------------------------------------------- 1 | 'Комментариев пока нет.', 5 | 'authentication_required' => 'Требуется авторизация', 6 | 'you_must_login_to_post_a_comment' => 'Войдите, чтобы оставить комментарий.', 7 | 'log_in' => 'Войти', 8 | 'reply' => 'Ответить', 9 | 'edit' => 'Редактировать', 10 | 'delete' => 'Удалить', 11 | 'edit_comment' => 'Редактирование комментария', 12 | 'update_your_message_here' => 'Отредактируйте свой комментарий здесь:', 13 | 'update' => 'Отредактировать', 14 | 'cancel' => 'Отменить', 15 | 'reply_to_comment' => 'Ответ на комментарий', 16 | 'markdown_cheatsheet' => 'Markdown разметка', 17 | 'submit' => 'Отправить', 18 | 'your_message_is_required' => 'Поле обязательно для заполнения.', 19 | 'enter_your_message_here' => 'Введите свой комментарий здесь:', 20 | 'enter_your_email_here' => 'Введите свою почту здесь:', 21 | 'enter_your_name_here' => 'Введите свое имя здесь:', 22 | ]; 23 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2020 Mario Bašić 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "laravelista/comments", 3 | "description": "Comments for Laravel.", 4 | "keywords": [ 5 | "laravel", 6 | "comments" 7 | ], 8 | "license": "MIT", 9 | "authors": [ 10 | { 11 | "name": "Mario Bašić", 12 | "email": "mario@laravelista.hr", 13 | "homepage": "https://laravelista.hr" 14 | } 15 | ], 16 | "require": { 17 | "php": "^8.0", 18 | "erusev/parsedown": "^1.7", 19 | "illuminate/database": "^9.0", 20 | "illuminate/http": "^9.0", 21 | "illuminate/pagination": "^9.0", 22 | "illuminate/routing": "^9.0", 23 | "illuminate/queue": "^9.0", 24 | "illuminate/support": "^9.0", 25 | "spatie/laravel-honeypot": "^4.1" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Laravelista\\Comments\\": "src/" 30 | } 31 | }, 32 | "extra": { 33 | "laravel": { 34 | "providers": [ 35 | "Laravelista\\Comments\\ServiceProvider" 36 | ] 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Commentable.php: -------------------------------------------------------------------------------- 1 | comments as $comment) { 22 | $comment->delete(); 23 | } 24 | }); 25 | } 26 | 27 | /** 28 | * Returns all comments for this model. 29 | */ 30 | public function comments() 31 | { 32 | return $this->morphMany(Config::get('comments.model'), 'commentable'); 33 | } 34 | 35 | /** 36 | * Returns only approved comments for this model. 37 | */ 38 | public function approvedComments() 39 | { 40 | return $this->morphMany(Config::get('comments.model'), 'commentable')->where('approved', true); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/CommentPolicy.php: -------------------------------------------------------------------------------- 1 | getKey() == $comment->commenter_id; 30 | } 31 | 32 | /** 33 | * Can user update the comment 34 | * 35 | * @param $user 36 | * @param Comment $comment 37 | * @return bool 38 | */ 39 | public function update($user, Comment $comment) : bool 40 | { 41 | return $user->getKey() == $comment->commenter_id; 42 | } 43 | 44 | /** 45 | * Can user reply to the comment 46 | * 47 | * @param $user 48 | * @param Comment $comment 49 | * @return bool 50 | */ 51 | public function reply($user, Comment $comment) : bool 52 | { 53 | return $user->getKey() != $comment->commenter_id; 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /migrations/2018_06_30_113500_create_comments_table.php: -------------------------------------------------------------------------------- 1 | bigIncrements('id'); 18 | 19 | $table->string('commenter_id')->nullable(); 20 | $table->string('commenter_type')->nullable(); 21 | $table->index(["commenter_id", "commenter_type"]); 22 | 23 | $table->string('guest_name')->nullable(); 24 | $table->string('guest_email')->nullable(); 25 | 26 | $table->string("commentable_type"); 27 | $table->string("commentable_id"); 28 | $table->index(["commentable_type", "commentable_id"]); 29 | 30 | $table->text('comment'); 31 | 32 | $table->boolean('approved')->default(true); 33 | 34 | $table->unsignedBigInteger('child_id')->nullable(); 35 | $table->foreign('child_id')->references('id')->on('comments')->onDelete('cascade'); 36 | 37 | $table->softDeletes(); 38 | $table->timestamps(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | * 45 | * @return void 46 | */ 47 | public function down() 48 | { 49 | Schema::dropIfExists('comments'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/WebCommentController.php: -------------------------------------------------------------------------------- 1 | commentService = $commentService; 19 | } 20 | 21 | /** 22 | * Creates a new comment for given model. 23 | */ 24 | public function store(Request $request) 25 | { 26 | $comment = $this->commentService->store($request); 27 | 28 | return Redirect::to(URL::previous() . '#comment-' . $comment->getKey()); 29 | } 30 | 31 | /** 32 | * Updates the message of the comment. 33 | */ 34 | public function update(Request $request, Comment $comment) 35 | { 36 | $comment = $this->commentService->update($request, $comment); 37 | 38 | return Redirect::to(URL::previous() . '#comment-' . $comment->getKey()); 39 | } 40 | 41 | /** 42 | * Deletes a comment. 43 | */ 44 | public function destroy(Comment $comment) 45 | { 46 | $this->commentService->destroy($comment); 47 | 48 | return Redirect::back(); 49 | } 50 | 51 | /** 52 | * Creates a reply "comment" to a comment. 53 | */ 54 | public function reply(Request $request, Comment $comment) 55 | { 56 | $reply = $this->commentService->reply($request, $comment); 57 | 58 | return Redirect::to(URL::previous() . '#comment-' . $reply->getKey()); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Comment.php: -------------------------------------------------------------------------------- 1 | 'boolean' 41 | ]; 42 | 43 | /** 44 | * The event map for the model. 45 | * 46 | * @var array 47 | */ 48 | protected $dispatchesEvents = [ 49 | 'created' => CommentCreated::class, 50 | 'updated' => CommentUpdated::class, 51 | 'deleted' => CommentDeleted::class, 52 | ]; 53 | 54 | /** 55 | * The user who posted the comment. 56 | */ 57 | public function commenter() 58 | { 59 | return $this->morphTo(); 60 | } 61 | 62 | /** 63 | * The model that was commented upon. 64 | */ 65 | public function commentable() 66 | { 67 | return $this->morphTo(); 68 | } 69 | 70 | /** 71 | * Returns all comments that this comment is the parent of. 72 | */ 73 | public function children() 74 | { 75 | return $this->hasMany(Config::get('comments.model'), 'child_id'); 76 | } 77 | 78 | /** 79 | * Returns the comment to which this comment belongs to. 80 | */ 81 | public function parent() 82 | { 83 | return $this->belongsTo(Config::get('comments.model'), 'child_id'); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /resources/views/_form.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
3 | @if($errors->has('commentable_type')) 4 | 7 | @endif 8 | @if($errors->has('commentable_id')) 9 | 12 | @endif 13 |
14 | @csrf 15 | @honeypot 16 | 17 | 18 | 19 | {{-- Guest commenting --}} 20 | @if(isset($guest_commenting) and $guest_commenting == true) 21 |
22 | 23 | 24 | @error('guest_name') 25 |
26 | {{ $message }} 27 |
28 | @enderror 29 |
30 |
31 | 32 | 33 | @error('guest_email') 34 |
35 | {{ $message }} 36 |
37 | @enderror 38 |
39 | @endif 40 | 41 |
42 | 43 | 44 |
45 | @lang('comments::comments.your_message_is_required') 46 |
47 | @lang('comments::comments.markdown_cheatsheet', ['url' => 'https://help.github.com/articles/basic-writing-and-formatting-syntax']) 48 |
49 | 50 |
51 |
52 |
53 |
-------------------------------------------------------------------------------- /resources/views/components/comments.blade.php: -------------------------------------------------------------------------------- 1 | @php 2 | if (isset($approved) and $approved == true) { 3 | $comments = $model->approvedComments; 4 | } else { 5 | $comments = $model->comments; 6 | } 7 | @endphp 8 | 9 | @if($comments->count() < 1) 10 |
@lang('comments::comments.there_are_no_comments')
11 | @endif 12 | 13 |
14 | @php 15 | $comments = $comments->sortBy('created_at'); 16 | 17 | if (isset($perPage)) { 18 | $page = request()->query('page', 1) - 1; 19 | 20 | $parentComments = $comments->where('child_id', ''); 21 | 22 | $slicedParentComments = $parentComments->slice($page * $perPage, $perPage); 23 | 24 | $m = Config::get('comments.model'); // This has to be done like this, otherwise it will complain. 25 | $modelKeyName = (new $m)->getKeyName(); // This defaults to 'id' if not changed. 26 | 27 | $slicedParentCommentsIds = $slicedParentComments->pluck($modelKeyName)->toArray(); 28 | 29 | // Remove parent Comments from comments. 30 | $comments = $comments->where('child_id', '!=', ''); 31 | 32 | $grouped_comments = new \Illuminate\Pagination\LengthAwarePaginator( 33 | $slicedParentComments->merge($comments)->groupBy('child_id'), 34 | $parentComments->count(), 35 | $perPage 36 | ); 37 | 38 | $grouped_comments->withPath(request()->url()); 39 | } else { 40 | $grouped_comments = $comments->groupBy('child_id'); 41 | } 42 | @endphp 43 | @foreach($grouped_comments as $comment_id => $comments) 44 | {{-- Process parent nodes --}} 45 | @if($comment_id == '') 46 | @foreach($comments as $comment) 47 | @include('comments::_comment', [ 48 | 'comment' => $comment, 49 | 'grouped_comments' => $grouped_comments, 50 | 'maxIndentationLevel' => $maxIndentationLevel ?? 3 51 | ]) 52 | @endforeach 53 | @endif 54 | @endforeach 55 |
56 | 57 | @isset ($perPage) 58 | {{ $grouped_comments->links() }} 59 | @endisset 60 | 61 | @auth 62 | @include('comments::_form') 63 | @elseif(Config::get('comments.guest_commenting') == true) 64 | @include('comments::_form', [ 65 | 'guest_commenting' => true 66 | ]) 67 | @else 68 |
69 |
70 |
@lang('comments::comments.authentication_required')
71 |

@lang('comments::comments.you_must_login_to_post_a_comment')

72 | @lang('comments::comments.log_in') 73 |
74 |
75 | @endauth 76 | -------------------------------------------------------------------------------- /config/comments.php: -------------------------------------------------------------------------------- 1 | \Laravelista\Comments\Comment::class, 11 | 12 | /** 13 | * You can customize the behaviour of these permissions by 14 | * creating your own and pointing to it here. 15 | */ 16 | 'permissions' => [ 17 | 'create-comment' => 'Laravelista\Comments\CommentPolicy@create', 18 | 'delete-comment' => 'Laravelista\Comments\CommentPolicy@delete', 19 | 'edit-comment' => 'Laravelista\Comments\CommentPolicy@update', 20 | 'reply-to-comment' => 'Laravelista\Comments\CommentPolicy@reply', 21 | ], 22 | 23 | /** 24 | * The Comment Controller. 25 | * Change this to your own implementation of the CommentController. 26 | * You can use the \Laravelista\Comments\CommentControllerInterface 27 | * or extend the \Laravelista\Comments\CommentController. 28 | */ 29 | 'controller' => '\Laravelista\Comments\WebCommentController', 30 | 31 | /** 32 | * Disable/enable the package routes. 33 | * If you want to completely take over the way this package handles 34 | * routes and controller logic, set this to false and provide your 35 | * own routes and controller for comments. 36 | */ 37 | 'routes' => true, 38 | 39 | /** 40 | * By default comments posted are marked as approved. If you want 41 | * to change this, set this option to true. Then, all comments 42 | * will need to be approved by setting the `approved` column to 43 | * `true` for each comment. 44 | * 45 | * To see only approved comments use this code in your view: 46 | * 47 | * @comments([ 48 | * 'model' => $book, 49 | * 'approved' => true 50 | * ]) 51 | * 52 | */ 53 | 'approval_required' => false, 54 | 55 | /** 56 | * Set this option to `true` to enable guest commenting. 57 | * 58 | * Visitors will be asked to provide their name and email 59 | * address in order to post a comment. 60 | */ 61 | 'guest_commenting' => false, 62 | 63 | /** 64 | * Set this option to `true` to enable soft deleting of comments. 65 | * 66 | * Comments will be soft deleted using laravels "softDeletes" trait. 67 | */ 68 | 'soft_deletes' => false, 69 | 70 | /** 71 | * Enable/disable the package provider to load migrations. 72 | * This option might be useful if you use multiple database connections. 73 | */ 74 | 'load_migrations' => true, 75 | 76 | /** 77 | * Enable/disable calling Paginator::useBootstrap() in the boot method 78 | * to prevent breaking non bootstrap based Site. 79 | */ 80 | 'paginator_use_bootstrap' => true, 81 | 82 | ]; 83 | -------------------------------------------------------------------------------- /src/ServiceProvider.php: -------------------------------------------------------------------------------- 1 | loadRoutesFrom(__DIR__ . '/routes.php'); 24 | } 25 | } 26 | 27 | /** 28 | * If load_migrations config is true (by default it is), 29 | * then load the package migrations, otherwise don't load 30 | * the migrations. 31 | */ 32 | protected function loadMigrations() 33 | { 34 | if (Config::get('comments.load_migrations') === true) { 35 | $this->loadMigrationsFrom(__DIR__ . '/../migrations'); 36 | } 37 | } 38 | 39 | /** 40 | * If for some reason you want to override the component. 41 | */ 42 | protected function includeBladeComponent() 43 | { 44 | Blade::include('comments::components.comments', 'comments'); 45 | } 46 | 47 | /** 48 | * Define permission defined in the config. 49 | */ 50 | protected function definePermissions() 51 | { 52 | foreach(Config::get('comments.permissions', []) as $permission => $policy) { 53 | Gate::define($permission, $policy); 54 | } 55 | } 56 | 57 | public function boot() 58 | { 59 | $this->loadRoutes(); 60 | 61 | $this->loadMigrations(); 62 | 63 | $this->loadViewsFrom(__DIR__ . '/../resources/views', 'comments'); 64 | 65 | $this->loadTranslationsFrom(__DIR__ . '/../resources/lang', 'comments'); 66 | 67 | $this->includeBladeComponent(); 68 | 69 | $this->definePermissions(); 70 | 71 | $this->publishes([ 72 | __DIR__.'/../migrations/' => App::databasePath('migrations') 73 | ], 'migrations'); 74 | 75 | $this->publishes([ 76 | __DIR__ . '/../resources/views' => App::resourcePath('views/vendor/comments'), 77 | ], 'views'); 78 | 79 | $this->publishes([ 80 | __DIR__ . '/../config/comments.php' => App::configPath('comments.php'), 81 | ], 'config'); 82 | 83 | $this->publishes([ 84 | __DIR__ . '/../resources/lang' => App::resourcePath('lang/vendor/comments'), 85 | ], 'translations'); 86 | 87 | Route::model('comment', Config::get('comments.model')); 88 | 89 | if (Config::get('comments.paginator_use_bootstrap', true)) { 90 | Paginator::useBootstrap(); 91 | } 92 | } 93 | 94 | public function register() 95 | { 96 | $this->mergeConfigFrom( 97 | __DIR__ . '/../config/comments.php', 98 | 'comments' 99 | ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/CommentService.php: -------------------------------------------------------------------------------- 1 | 'required|string|max:255', 28 | 'guest_email' => 'required|string|email|max:255', 29 | ]; 30 | } 31 | 32 | // Merge guest rules, if any, with normal validation rules. 33 | Validator::make($request->all(), array_merge($guest_rules ?? [], [ 34 | 'commentable_type' => 'required|string', 35 | 'commentable_id' => 'required|string|min:1', 36 | 'message' => 'required|string' 37 | ]))->validate(); 38 | 39 | $model = $request->commentable_type::findOrFail($request->commentable_id); 40 | 41 | $commentClass = Config::get('comments.model'); 42 | $comment = new $commentClass; 43 | 44 | if (!Auth::check()) { 45 | $comment->guest_name = $request->guest_name; 46 | $comment->guest_email = $request->guest_email; 47 | } else { 48 | $comment->commenter()->associate(Auth::user()); 49 | } 50 | 51 | $comment->commentable()->associate($model); 52 | $comment->comment = $request->message; 53 | $comment->approved = !Config::get('comments.approval_required'); 54 | $comment->save(); 55 | 56 | return $comment; 57 | } 58 | 59 | /** 60 | * Handles updating the message of the comment. 61 | * @return mixed the configured comment-model 62 | */ 63 | public function update(Request $request, Comment $comment) 64 | { 65 | Gate::authorize('edit-comment', $comment); 66 | 67 | Validator::make($request->all(), [ 68 | 'message' => 'required|string' 69 | ])->validate(); 70 | 71 | $comment->update([ 72 | 'comment' => $request->message 73 | ]); 74 | 75 | return $comment; 76 | } 77 | 78 | /** 79 | * Handles deleting a comment. 80 | * @return mixed the configured comment-model 81 | */ 82 | public function destroy(Comment $comment): void 83 | { 84 | Gate::authorize('delete-comment', $comment); 85 | 86 | if (Config::get('comments.soft_deletes') == true) { 87 | $comment->delete(); 88 | } else { 89 | $comment->forceDelete(); 90 | } 91 | } 92 | 93 | /** 94 | * Handles creating a reply "comment" to a comment. 95 | * @return mixed the configured comment-model 96 | */ 97 | public function reply(Request $request, Comment $comment) 98 | { 99 | Gate::authorize('reply-to-comment', $comment); 100 | 101 | Validator::make($request->all(), [ 102 | 'message' => 'required|string' 103 | ])->validate(); 104 | 105 | $commentClass = Config::get('comments.model'); 106 | $reply = new $commentClass; 107 | $reply->commenter()->associate(Auth::user()); 108 | $reply->commentable()->associate($comment->commentable); 109 | $reply->parent()->associate($comment); 110 | $reply->comment = $request->message; 111 | $reply->approved = !Config::get('comments.approval_required'); 112 | $reply->save(); 113 | 114 | return $reply; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /UPGRADE.md: -------------------------------------------------------------------------------- 1 | # Upgrading from older versions 2 | 3 | Be sure to update your version of the config file to match the latest version of the package. 4 | 5 | ## TODO 6 | 7 | - [ ] document new config option `paginator_use_bootstrap`. 8 | 9 | 10 | ## Support for soft deletes (`3.4.0`) 11 | 12 | If you are updating an already existing database table `comments` and want support for soft deletes **(new installations get this by default)**, then create a new migration with `php artisan make:migration add_soft_delete_column_to_comments_table` and paste this code inside: 13 | 14 | ``` 15 | softDeletes(); 32 | }); 33 | } 34 | } 35 | ``` 36 | 37 | Finally, run `php artisan migrate`. 38 | 39 | 40 | ## Support for guest commenting 41 | 42 | If you are updating an already existing database table `comments` and want support for guest commenting **(new installations get this by default)**, then create a new migration with `php artisan make:migration add_guest_commenting_columns_to_comments_table` and paste this code inside: 43 | 44 | ``` 45 | string('commenter_id')->nullable()->change(); 62 | $table->string('commenter_type')->nullable()->change(); 63 | 64 | $table->string('guest_name')->nullable(); 65 | $table->string('guest_email')->nullable(); 66 | }); 67 | } 68 | } 69 | ``` 70 | 71 | Finally, run `php artisan migrate`. 72 | 73 | 74 | ## Support for approving comments 75 | 76 | If you are updating an already existing database table `comments` and want support for approving comments **(new installations get this by default)**, then create a new migration with `php artisan make:migration add_approved_column_to_comments_table` and paste this code inside: 77 | 78 | ``` 79 | boolean('approved')->default(true)->nullable(); 96 | }); 97 | } 98 | } 99 | ``` 100 | 101 | Finally, run `php artisan migrate`. 102 | 103 | 104 | ## Support for multiple user models 105 | 106 | If you are updating an already existing database table `comments` and want support for multiple user models **(new installations get this by default)**, then create a new migration with `php artisan make:migration add_commenter_type_column_to_comments_table` and paste this code inside: 107 | 108 | ``` 109 | string('commenter_id')->change(); 127 | $table->string('commenter_type')->nullable(); 128 | }); 129 | 130 | DB::table('comments')->update([ 131 | 'commenter_type' => '\App\User' 132 | ]); 133 | } 134 | } 135 | ``` 136 | 137 | Then, add `doctrine/dbal` dependency with: 138 | 139 | ``` 140 | composer require doctrine/dbal 141 | ``` 142 | 143 | Finally, run `php artisan migrate`. 144 | 145 | 146 | ## Support for non-integer IDs 147 | 148 | If you are updating an already existing database table `comments` and want support for non-integer IDs **(new installations get this by default)**, then create a new migration with `php artisan make:migration allow_commentable_id_to_be_string` and paste this code inside: 149 | 150 | ``` 151 | string('commentable_id')->change(); 168 | }); 169 | } 170 | } 171 | ``` 172 | 173 | Then, add `doctrine/dbal` dependency with: 174 | 175 | ``` 176 | composer require doctrine/dbal 177 | ``` 178 | 179 | Finally, run `php artisan migrate`. 180 | 181 | -------------------------------------------------------------------------------- /resources/views/_comment.blade.php: -------------------------------------------------------------------------------- 1 | @inject('markdown', 'Parsedown') 2 | @php 3 | // TODO: There should be a better place for this. 4 | $markdown->setSafeMode(true); 5 | @endphp 6 | 7 |
8 | {{ $comment->commenter->name ?? $comment->guest_name }} Avatar 9 |
10 |
{{ $comment->commenter->name ?? $comment->guest_name }} - {{ $comment->created_at->diffForHumans() }}
11 |
{!! $markdown->line($comment->comment) !!}
12 | 13 |
14 | @can('reply-to-comment', $comment) 15 | 16 | @endcan 17 | @can('edit-comment', $comment) 18 | 19 | @endcan 20 | @can('delete-comment', $comment) 21 | @lang('comments::comments.delete') 22 | 26 | @endcan 27 |
28 | 29 | @can('edit-comment', $comment) 30 | 57 | @endcan 58 | 59 | @can('reply-to-comment', $comment) 60 | 86 | @endcan 87 | 88 |
{{-- Margin bottom --}} 89 | 90 | 97 | 98 | {{-- Recursion for children --}} 99 | @if($grouped_comments->has($comment->getKey()) && $indentationLevel <= $maxIndentationLevel) 100 | {{-- TODO: Don't repeat code. Extract to a new file and include it. --}} 101 | @foreach($grouped_comments[$comment->getKey()] as $child) 102 | @include('comments::_comment', [ 103 | 'comment' => $child, 104 | 'grouped_comments' => $grouped_comments 105 | ]) 106 | @endforeach 107 | @endif 108 | 109 |
110 |
111 | 112 | {{-- Recursion for children --}} 113 | @if($grouped_comments->has($comment->getKey()) && $indentationLevel > $maxIndentationLevel) 114 | {{-- TODO: Don't repeat code. Extract to a new file and include it. --}} 115 | @foreach($grouped_comments[$comment->getKey()] as $child) 116 | @include('comments::_comment', [ 117 | 'comment' => $child, 118 | 'grouped_comments' => $grouped_comments 119 | ]) 120 | @endforeach 121 | @endif -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comments 2 | 3 | Comments is a Laravel package. With it you can easily implement native comments for your application. 4 | 5 | [![Become a Patron](https://img.shields.io/badge/Become%20a-Patron-f96854.svg?style=for-the-badge)](https://www.patreon.com/laravelista) 6 | 7 | 8 | ## Overview 9 | 10 | This package can be used to comment on any model you have in your application. 11 | 12 | All comments are stored in a single table with a polymorphic relation for content and a polymorphic relation for the user who posted the comment. 13 | 14 | 15 | ### Features 16 | 17 | - View comments 18 | - Create comments 19 | - Delete comments 20 | - Edit comments 21 | - Reply to comments 22 | - Authorization rules 23 | - Support localization 24 | - Dispatch events 25 | - Route, Controller, Comment, Migration & View customizations 26 | - Support for non-integer IDs 27 | - Support for multiple User models 28 | - Solved N+1 query problem 29 | - Comment approval (opt-in) 30 | - Guest commenting (opt-in) 31 | - Pagination (opt-in) 32 | - Soft deletes (opt-in) 33 | - Works with custom ID columns 34 | - Optionally load package migrations [NEW] 35 | - Configure maximum indentation level [NEW] 36 | 37 | 38 | ### Screenshots 39 | 40 | Here are a few screenshots. 41 | 42 | No comments & guest: 43 | 44 | ![](https://i.imgur.com/9df4Xun.png) 45 | 46 | No comments & logged in: 47 | 48 | ![](https://i.imgur.com/ALI6GbR.png) 49 | 50 | One comment: 51 | 52 | ![](https://i.imgur.com/9wBNiy2.png) 53 | 54 | One comment edit form: 55 | 56 | ![](https://i.imgur.com/cxDh34O.png) 57 | 58 | Two comments from different users: 59 | 60 | ![](https://i.imgur.com/2P5u25x.png) 61 | 62 | 63 | ### Tutorials & articles 64 | 65 | I plan to expand this chapter with more tutorials and articles. If you write something about this package let me know, so that I can update this chapter. 66 | 67 | **Screencasts:** 68 | 69 | - [Adding comments to your Laravel application](https://www.youtube.com/watch?v=YhA0CSX1HIg) by Andre Madarang. 70 | 71 | 72 | ## Installation 73 | 74 | From the command line: 75 | 76 | ```bash 77 | composer require laravelista/comments 78 | ``` 79 | 80 | 81 | ### Run migrations 82 | 83 | We need to create the table for comments. 84 | 85 | ```bash 86 | php artisan migrate 87 | ``` 88 | 89 | 90 | ### Add Commenter trait to your User model 91 | 92 | Add the `Commenter` trait to your User model so that you can retrieve the comments for a user: 93 | 94 | ```php 95 | use Laravelista\Comments\Commenter; 96 | 97 | class User extends Authenticatable 98 | { 99 | use Notifiable, Commenter; 100 | } 101 | ``` 102 | 103 | 104 | ### Add Commentable trait to models 105 | 106 | Add the `Commentable` trait to the model for which you want to enable comments for: 107 | 108 | ```php 109 | use Laravelista\Comments\Commentable; 110 | 111 | class Product extends Model 112 | { 113 | use Commentable; 114 | } 115 | ``` 116 | 117 | 118 | ### Publish Config & configure (optional) 119 | 120 | Publish the config file (optional): 121 | 122 | ```bash 123 | php artisan vendor:publish --provider="Laravelista\Comments\ServiceProvider" --tag=config 124 | ``` 125 | 126 | 127 | ### Publish views (customization) 128 | 129 | The default UI is made for Bootstrap 4, but you can change it however you want. 130 | 131 | ```bash 132 | php artisan vendor:publish --provider="Laravelista\Comments\ServiceProvider" --tag=views 133 | ``` 134 | 135 | 136 | ### Publish Migrations (customization) 137 | 138 | You can publish migration to allow you to have more control over your table 139 | 140 | ```bash 141 | php artisan vendor:publish --provider="Laravelista\Comments\ServiceProvider" --tag=migrations 142 | ``` 143 | 144 | 145 | ### Publish translations (customization) 146 | 147 | The package currently only supports English, but I am open to PRs for other languages. 148 | 149 | ```bash 150 | php artisan vendor:publish --provider="Laravelista\Comments\ServiceProvider" --tag=translations 151 | ``` 152 | 153 | 154 | ## Usage 155 | 156 | In the view where you want to display comments, place this code and modify it: 157 | 158 | ``` 159 | @comments(['model' => $book]) 160 | ``` 161 | 162 | In the example above we are setting the `commentable_type` to the class of the book. We are also passing the `commentable_id` the `id` of the book so that we know to which book the comments relate to. Behind the scenes, the package detects the currently logged in user if any. 163 | 164 | If you open the page containing the view where you have placed the above code, you should see a working comments form. 165 | 166 | 167 | ### View only approved comments 168 | 169 | To view only approved comments, use this code: 170 | 171 | ``` 172 | @comments([ 173 | 'model' => $book, 174 | 'approved' => true 175 | ]) 176 | ``` 177 | 178 | 179 | ### Paginate comments 180 | 181 | Pagination paginates by top level comments only, meaning that if you specify the number of comments per page to be 1, and that one comment has 100 replies, it will display that one comment and all of its replies. 182 | 183 | It was not possible to do it any other way, because if I paginate by all comments (parent and child) you will end up with blank pages since the comments components loops parent comments first and then uses recursion for replies. 184 | 185 | To use pagination, use this code: 186 | 187 | ``` 188 | @comments([ 189 | 'model' => $user, 190 | 'perPage' => 2 191 | ]) 192 | ``` 193 | 194 | Replace `2` with any number you want. 195 | 196 | ### Configure maximum indentation level 197 | 198 | By default the replies go up to level three. After that they are "mashed" at that level. 199 | 200 | ``` 201 | - 0 202 | - 1 203 | - 2 204 | - 3 205 | ``` 206 | 207 | You can configure the maximum indentation level like so: 208 | 209 | ``` 210 | @comments([ 211 | 'model' => $user, 212 | 'maxIndentationLevel' => 1 213 | ]) 214 | ``` 215 | 216 | 217 | ## Events 218 | 219 | This package fires events to let you know when things happen. 220 | 221 | - `Laravelista\Comments\Events\CommentCreated` 222 | - `Laravelista\Comments\Events\CommentUpdated` 223 | - `Laravelista\Comments\Events\CommentDeleted` 224 | 225 | 226 | ## REST API 227 | 228 | To change the controller or the routes, see the config. 229 | 230 | ``` 231 | Route::post('comments', '\Laravelista\Comments\CommentController@store')->name('comments.store'); 232 | Route::delete('comments/{comment}', '\Laravelista\Comments\CommentController@destroy')->name('comments.destroy'); 233 | Route::put('comments/{comment}', '\Laravelista\Comments\CommentController@update')->name('comments.update'); 234 | Route::post('comments/{comment}', '\Laravelista\Comments\CommentController@reply')->name('comments.reply'); 235 | ``` 236 | 237 | 238 | ### POST `/comments` 239 | 240 | Request data: 241 | 242 | ``` 243 | 'commentable_type' => 'required|string', 244 | 'commentable_id' => 'required|string|min:1', 245 | 'message' => 'required|string' 246 | ``` 247 | 248 | 249 | ### PUT `/comments/{comment}` 250 | 251 | - {comment} - Comment ID. 252 | 253 | Request data: 254 | 255 | ``` 256 | 'message' => 'required|string' 257 | ``` 258 | 259 | 260 | ### POST `/comments/{comment}` 261 | 262 | - {comment} - Comment ID. 263 | 264 | Request data: 265 | 266 | ``` 267 | 'message' => 'required|string' 268 | ``` 269 | 270 | 271 | ## Upgrading from older versions (troubleshoot) 272 | 273 | Before creating an issue, read [this](./UPGRADE.md). 274 | 275 | 276 | ## Sponsors & Backers 277 | 278 | I would like to extend my thanks to the following sponsors & backers for funding my open-source journey. If you are interested in becoming a sponsor or backer, please visit the [Backers page](https://mariobasic.com/backers). 279 | 280 | 281 | ## Contributing 282 | 283 | Thank you for considering contributing to Comments! The contribution guide can be found [Here](https://mariobasic.com/contributing). 284 | 285 | 286 | ## Code of Conduct 287 | 288 | In order to ensure that the open-source community is welcoming to all, please review and abide by the [Code of Conduct](https://mariobasic.com/code-of-conduct). 289 | 290 | 291 | ## License 292 | 293 | Comments is open-source software licensed under the [MIT license](https://opensource.org/licenses/MIT). 294 | --------------------------------------------------------------------------------