118
common/Comments/Comment.php
Executable file
118
common/Comments/Comment.php
Executable file
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Core\BaseModel;
|
||||
use Common\Files\Traits\HandlesEntryPaths;
|
||||
use Common\Votes\OrdersByWeightedScore;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphTo;
|
||||
|
||||
class Comment extends BaseModel
|
||||
{
|
||||
use HandlesEntryPaths, HasFactory, OrdersByWeightedScore;
|
||||
|
||||
const MODEL_TYPE = 'comment';
|
||||
|
||||
protected $guarded = ['id'];
|
||||
|
||||
protected $hidden = ['commentable_type', 'commentable_id', 'path'];
|
||||
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'deleted' => 'boolean',
|
||||
];
|
||||
|
||||
protected $appends = ['depth', 'model_type'];
|
||||
|
||||
public function votes(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommentVote::class);
|
||||
}
|
||||
|
||||
public function reports(): HasMany
|
||||
{
|
||||
return $this->hasMany(CommentReport::class);
|
||||
}
|
||||
|
||||
public function user(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(User::class);
|
||||
}
|
||||
|
||||
public function commentable(): MorphTo
|
||||
{
|
||||
return $this->morphTo();
|
||||
}
|
||||
|
||||
public function scopeRootOnly(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNull('parent_id');
|
||||
}
|
||||
|
||||
public function scopeChildrenOnly(Builder $builder): Builder
|
||||
{
|
||||
return $builder->whereNotNull('parent_id');
|
||||
}
|
||||
|
||||
public function getDepthAttribute(): int
|
||||
{
|
||||
if (!$this->path || !$this->parent_id) {
|
||||
return 0;
|
||||
}
|
||||
return count(explode('/', $this->getRawOriginal('path')));
|
||||
}
|
||||
|
||||
public function toNormalizedArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'name' => $this->content,
|
||||
'model_type' => self::MODEL_TYPE,
|
||||
];
|
||||
}
|
||||
|
||||
public function toSearchableArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'content' => $this->content,
|
||||
'parent_id' => $this->parent_id,
|
||||
'user_id' => $this->user_id,
|
||||
'deleted' => $this->deleted,
|
||||
'commentable_id' => $this->commentable_id,
|
||||
'commentable_type' => $this->commentable_type,
|
||||
'created_at' => $this->created_at->timestamp ?? '_null',
|
||||
'updated_at' => $this->updated_at->timestamp ?? '_null',
|
||||
];
|
||||
}
|
||||
|
||||
public static function filterableFields(): array
|
||||
{
|
||||
return [
|
||||
'id',
|
||||
'parent_id',
|
||||
'user_id',
|
||||
'deleted',
|
||||
'commentable_id',
|
||||
'commentable_type',
|
||||
'created_at',
|
||||
'updated_at',
|
||||
];
|
||||
}
|
||||
|
||||
protected static function newFactory()
|
||||
{
|
||||
return CommentFactory::new();
|
||||
}
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
29
common/Comments/CommentFactory.php
Executable file
29
common/Comments/CommentFactory.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use App\Models\User;
|
||||
use Arr;
|
||||
use Common\Auth\Roles\Role;
|
||||
use Common\Billing\Models\Product;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
|
||||
class CommentFactory extends Factory
|
||||
{
|
||||
protected $model = Comment::class;
|
||||
|
||||
public function definition(): array
|
||||
{
|
||||
return [
|
||||
'content' => $this->faker->realText(),
|
||||
'commentable_type' => Arr::random([
|
||||
User::MODEL_TYPE,
|
||||
Product::MODEL_TYPE,
|
||||
Role::MODEL_TYPE,
|
||||
]),
|
||||
'commentable_id' => Arr::random([1, 2, 3, 4, 5]),
|
||||
'user_id' => Arr::random([1, 2, 3, 4, 5]),
|
||||
'parent_id' => Arr::random([1, null]),
|
||||
];
|
||||
}
|
||||
}
|
||||
51
common/Comments/CommentPolicy.php
Executable file
51
common/Comments/CommentPolicy.php
Executable file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use App\Models\User;
|
||||
use Common\Auth\BaseUser;
|
||||
use Common\Core\Policies\BasePolicy;
|
||||
|
||||
class CommentPolicy extends BasePolicy
|
||||
{
|
||||
public function vote(User $user)
|
||||
{
|
||||
return $user->hasPermission('comments.create');
|
||||
}
|
||||
|
||||
public function index(?BaseUser $user, $userId = null)
|
||||
{
|
||||
return $user->hasPermission('comments.view') ||
|
||||
$user->id === (int) $userId;
|
||||
}
|
||||
|
||||
public function show(?BaseUser $user, Comment $comment)
|
||||
{
|
||||
return $user->hasPermission('comments.view') ||
|
||||
$comment->user_id === $user->id;
|
||||
}
|
||||
|
||||
public function store(BaseUser $user)
|
||||
{
|
||||
return $user->id && $user->hasPermission('comments.create');
|
||||
}
|
||||
|
||||
public function update(BaseUser $user, ?Comment $comment = null)
|
||||
{
|
||||
return $user->hasPermission('comments.update') ||
|
||||
($comment && $comment->user_id === $user->id);
|
||||
}
|
||||
|
||||
public function destroy(BaseUser $user, $commentIds)
|
||||
{
|
||||
if ($user->hasPermission('comments.delete')) {
|
||||
return true;
|
||||
} else {
|
||||
$dbCount = app(Comment::class)
|
||||
->whereIn('id', $commentIds)
|
||||
->where('user_id', $user->id)
|
||||
->count();
|
||||
return $dbCount === count($commentIds);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
common/Comments/CommentReport.php
Executable file
22
common/Comments/CommentReport.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class CommentReport extends Model
|
||||
{
|
||||
protected $guarded = ['id'];
|
||||
protected $casts = [
|
||||
'id' => 'integer',
|
||||
'user_id' => 'integer',
|
||||
'comment_id' => 'integer',
|
||||
];
|
||||
|
||||
const MODEL_TYPE = 'comment_report';
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
15
common/Comments/CommentVote.php
Executable file
15
common/Comments/CommentVote.php
Executable file
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use Common\Votes\Vote;
|
||||
|
||||
class CommentVote extends Vote
|
||||
{
|
||||
const MODEL_TYPE = 'comment_vote';
|
||||
|
||||
public static function getModelTypeAttribute(): string
|
||||
{
|
||||
return self::MODEL_TYPE;
|
||||
}
|
||||
}
|
||||
142
common/Comments/Controllers/CommentController.php
Executable file
142
common/Comments/Controllers/CommentController.php
Executable file
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments\Controllers;
|
||||
|
||||
use Common\Comments\Comment;
|
||||
use Common\Comments\CrupdateComment;
|
||||
use Common\Comments\CrupdateCommentRequest;
|
||||
use Common\Core\BaseController;
|
||||
use Common\Database\Datasource\Datasource;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
class CommentController extends BaseController
|
||||
{
|
||||
public function __construct(
|
||||
protected Comment $comment,
|
||||
protected Request $request,
|
||||
) {
|
||||
}
|
||||
|
||||
public function index(): Response
|
||||
{
|
||||
$userId = $this->request->get('userId');
|
||||
$this->authorize('index', [Comment::class, $userId]);
|
||||
|
||||
$builder = $this->comment->with(['user']);
|
||||
|
||||
// will need to specify this outside of filters on edit title comments page
|
||||
if (request('commentable_id') && request('commentable_type')) {
|
||||
$builder->where([
|
||||
'commentable_id' => request('commentable_id'),
|
||||
'commentable_type' => request('commentable_type'),
|
||||
]);
|
||||
}
|
||||
|
||||
$dataSource = new Datasource(
|
||||
$builder,
|
||||
$this->request->all(),
|
||||
);
|
||||
|
||||
$pagination = $dataSource->paginate();
|
||||
|
||||
$pagination->transform(function (Comment $comment) {
|
||||
if ($comment->relationLoaded('commentable') && $comment->commentable) {
|
||||
$normalized = $comment->commentable->toNormalizedArray();
|
||||
$comment->unsetRelation('commentable');
|
||||
$comment->setAttribute('commentable', $normalized);
|
||||
}
|
||||
return $comment;
|
||||
});
|
||||
|
||||
return $this->success(['pagination' => $pagination]);
|
||||
}
|
||||
|
||||
public function show(Comment $comment): Response
|
||||
{
|
||||
$this->authorize('show', $comment);
|
||||
|
||||
return $this->success(['comment' => $comment]);
|
||||
}
|
||||
|
||||
public function store(CrupdateCommentRequest $request): Response
|
||||
{
|
||||
$this->authorize('store', Comment::class);
|
||||
|
||||
$comment = app(CrupdateComment::class)->execute($request->all());
|
||||
|
||||
return $this->success(['comment' => $comment]);
|
||||
}
|
||||
|
||||
public function update(
|
||||
Comment $comment,
|
||||
CrupdateCommentRequest $request,
|
||||
): Response {
|
||||
$this->authorize('store', $comment);
|
||||
|
||||
$comment = app(CrupdateComment::class)->execute(
|
||||
$request->all(),
|
||||
$comment,
|
||||
);
|
||||
|
||||
return $this->success(['comment' => $comment]);
|
||||
}
|
||||
|
||||
public function destroy(string $ids): Response
|
||||
{
|
||||
$commentIds = explode(',', $ids);
|
||||
$this->authorize('destroy', [Comment::class, $commentIds]);
|
||||
|
||||
$allDeleted = [];
|
||||
$allMarkedAsDeleted = [];
|
||||
|
||||
$this->comment
|
||||
->whereIn('id', $commentIds)
|
||||
->chunkById(100, function (Collection $comments) use (
|
||||
&$allDeleted,
|
||||
&$allMarkedAsDeleted,
|
||||
) {
|
||||
$toMarkAsDeleted = [];
|
||||
$toDelete = [];
|
||||
foreach ($comments as $comment) {
|
||||
if ($comment->allChildren()->count() > 1) {
|
||||
$toMarkAsDeleted[] = $comment->id;
|
||||
} else {
|
||||
$toDelete[] = $comment->id;
|
||||
}
|
||||
}
|
||||
if (!empty($toMarkAsDeleted)) {
|
||||
$this->comment
|
||||
->whereIn('id', $toMarkAsDeleted)
|
||||
->update(['deleted' => true]);
|
||||
}
|
||||
if (!empty($toDelete)) {
|
||||
$this->comment->whereIn('id', $toDelete)->delete();
|
||||
}
|
||||
$allDeleted = array_merge($allDeleted, $toDelete);
|
||||
$allMarkedAsDeleted = array_merge(
|
||||
$allMarkedAsDeleted,
|
||||
$toMarkAsDeleted,
|
||||
);
|
||||
});
|
||||
|
||||
return $this->success([
|
||||
'allDeleted' => $allDeleted,
|
||||
'allMarkedAsDeleted' => $allMarkedAsDeleted,
|
||||
]);
|
||||
}
|
||||
|
||||
public function restore()
|
||||
{
|
||||
$this->authorize('update', Comment::class);
|
||||
|
||||
$commentIds = $this->request->get('commentIds');
|
||||
|
||||
$this->comment
|
||||
->whereIn('id', $commentIds)
|
||||
->update(['deleted' => false]);
|
||||
|
||||
return $this->success();
|
||||
}
|
||||
}
|
||||
29
common/Comments/Controllers/CommentableController.php
Executable file
29
common/Comments/Controllers/CommentableController.php
Executable file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments\Controllers;
|
||||
|
||||
use Common\Comments\PaginateModelComments;
|
||||
use Common\Core\BaseController;
|
||||
|
||||
class CommentableController extends BaseController
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$modelType = request('commentable_type');
|
||||
$modelId = request('commentable_id');
|
||||
|
||||
if (!$modelType || !$modelId) {
|
||||
abort(404);
|
||||
}
|
||||
|
||||
$commentable = app(modelTypeToNamespace($modelType))->findOrFail(
|
||||
$modelId,
|
||||
);
|
||||
|
||||
$pagination = app(PaginateModelComments::class)->execute($commentable);
|
||||
|
||||
return $this->success([
|
||||
'pagination' => $pagination,
|
||||
]);
|
||||
}
|
||||
}
|
||||
53
common/Comments/CrupdateComment.php
Executable file
53
common/Comments/CrupdateComment.php
Executable file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use App\Models\User;
|
||||
use Auth;
|
||||
use Common\Comments\Notifications\CommentReceivedReply;
|
||||
use Illuminate\Support\Arr;
|
||||
|
||||
class CrupdateComment
|
||||
{
|
||||
public function execute(
|
||||
array $data,
|
||||
Comment $initialComment = null,
|
||||
): Comment {
|
||||
if (!$initialComment) {
|
||||
$comment = new Comment([
|
||||
'user_id' => Auth::id(),
|
||||
]);
|
||||
} else {
|
||||
$comment = $initialComment;
|
||||
}
|
||||
|
||||
$inReplyTo = Arr::get($data, 'inReplyTo');
|
||||
|
||||
// specific app might need to store
|
||||
// some extra data along with comment
|
||||
$attributes = Arr::except($data, 'inReplyTo');
|
||||
if ($inReplyTo) {
|
||||
$attributes['parent_id'] = $inReplyTo['id'];
|
||||
}
|
||||
|
||||
if (isset($attributes['commentable_type'])) {
|
||||
// track => App\Track
|
||||
$attributes['commentable_type'] = $data['commentable_type'];
|
||||
}
|
||||
$comment->fill($attributes)->save();
|
||||
|
||||
$comment->generatePath();
|
||||
|
||||
if (
|
||||
!$initialComment &&
|
||||
$inReplyTo &&
|
||||
$inReplyTo['user']['id'] !== Auth::id()
|
||||
) {
|
||||
app(User::class)
|
||||
->find($inReplyTo['user']['id'])
|
||||
->notify(new CommentReceivedReply($comment, $inReplyTo));
|
||||
}
|
||||
|
||||
return $comment;
|
||||
}
|
||||
}
|
||||
23
common/Comments/CrupdateCommentRequest.php
Executable file
23
common/Comments/CrupdateCommentRequest.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use Auth;
|
||||
use Common\Core\BaseFormRequest;
|
||||
|
||||
class CrupdateCommentRequest extends BaseFormRequest
|
||||
{
|
||||
public function rules(): array
|
||||
{
|
||||
$required = $this->getMethod() === 'POST' ? 'required' : '';
|
||||
$ignore =
|
||||
$this->getMethod() === 'PUT' ? $this->route('comment')->id : '';
|
||||
$userId = $this->route('comment')
|
||||
? $this->route('comment')->user_id
|
||||
: Auth::id();
|
||||
|
||||
return [
|
||||
'content' => 'required|string|max:1000|min:3',
|
||||
];
|
||||
}
|
||||
}
|
||||
43
common/Comments/LoadChildComments.php
Executable file
43
common/Comments/LoadChildComments.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
class LoadChildComments
|
||||
{
|
||||
public function execute(
|
||||
Model $commentable,
|
||||
Collection $rootComments,
|
||||
): Collection {
|
||||
$paths = $rootComments
|
||||
->map(function (Comment $comment) {
|
||||
$path = $comment->getRawOriginal('path');
|
||||
return "LIKE '$path%'";
|
||||
})
|
||||
->implode(' OR path ');
|
||||
|
||||
$childComments = app(Comment::class)
|
||||
->with(['user' => fn($builder) => $builder->compact()])
|
||||
->where('commentable_id', $commentable->id)
|
||||
->where('commentable_type', $commentable->getMorphClass())
|
||||
->childrenOnly()
|
||||
->where(fn(Builder $builder) => $builder->whereRaw("path $paths"))
|
||||
->orderBy('path', 'asc')
|
||||
->orderBy('created_at', 'desc')
|
||||
->orderByWeightedScore()
|
||||
->limit(100)
|
||||
->get();
|
||||
|
||||
$childComments->each(function ($child) use ($rootComments) {
|
||||
$index = $rootComments->search(
|
||||
fn($parent) => $parent['id'] === $child['parent_id'],
|
||||
);
|
||||
$rootComments->splice($index + 1, 0, [$child]);
|
||||
});
|
||||
|
||||
return $rootComments;
|
||||
}
|
||||
}
|
||||
75
common/Comments/Notifications/CommentReceivedReply.php
Executable file
75
common/Comments/Notifications/CommentReceivedReply.php
Executable file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments\Notifications;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Services\UrlGenerator;
|
||||
use Common\Comments\Comment;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Notifications\Notification;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class CommentReceivedReply extends Notification
|
||||
{
|
||||
use Queueable;
|
||||
|
||||
public Model $commentable;
|
||||
|
||||
public function __construct(
|
||||
public Comment $newComment,
|
||||
public array $originalComment,
|
||||
) {
|
||||
$this->newComment = $newComment;
|
||||
$this->originalComment = $originalComment;
|
||||
$this->commentable = app(
|
||||
modelTypeToNamespace($newComment['commentable_type']),
|
||||
)->find($newComment['commentable_id']);
|
||||
}
|
||||
|
||||
public function via(User $notifiable): array
|
||||
{
|
||||
return ['database'];
|
||||
}
|
||||
|
||||
public function toArray(User $notifiable): array
|
||||
{
|
||||
$username = $this->newComment['user']['display_name'];
|
||||
$commentable = $this->commentable->toNormalizedArray();
|
||||
|
||||
return [
|
||||
'image' => $this->originalComment['user']['avatar'],
|
||||
'mainAction' => [
|
||||
'action' => app(UrlGenerator::class)->generate(
|
||||
$this->commentable,
|
||||
),
|
||||
],
|
||||
'lines' => [
|
||||
[
|
||||
'content' => __(':username replied to your comment:', [
|
||||
'username' => $username,
|
||||
]),
|
||||
'action' => [
|
||||
'action' => app(UrlGenerator::class)->user(
|
||||
$this->newComment['user'],
|
||||
),
|
||||
'label' => __('View user'),
|
||||
],
|
||||
'type' => 'secondary',
|
||||
],
|
||||
[
|
||||
'content' =>
|
||||
'"' .
|
||||
Str::limit($this->newComment['content'], 180) .
|
||||
'"',
|
||||
'icon' => 'comment',
|
||||
'type' => 'primary',
|
||||
],
|
||||
[
|
||||
'content' => __('on') . " {$commentable['name']}",
|
||||
'type' => 'secondary',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
43
common/Comments/PaginateModelComments.php
Executable file
43
common/Comments/PaginateModelComments.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
namespace Common\Comments;
|
||||
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class PaginateModelComments
|
||||
{
|
||||
public function execute(Model $commentable): array
|
||||
{
|
||||
$pagination = $commentable
|
||||
->comments()
|
||||
->rootOnly()
|
||||
->with([
|
||||
'user' => fn($builder) => $builder->compact(),
|
||||
])
|
||||
->paginate(request('perPage') ?? 25);
|
||||
|
||||
$comments = app(LoadChildComments::class)->execute(
|
||||
$commentable,
|
||||
Collection::make($pagination->items()),
|
||||
);
|
||||
|
||||
$comments->load([
|
||||
'votes' => fn($builder) => $builder->withCurrentUserVotes(),
|
||||
]);
|
||||
|
||||
$comments->transform(function (Comment $comment) {
|
||||
if ($comment->deleted) {
|
||||
$comment->content = null;
|
||||
}
|
||||
$comment->current_vote = $comment->votes->first()?->vote_type;
|
||||
$comment->unsetRelation('votes');
|
||||
return $comment;
|
||||
});
|
||||
|
||||
$pagination = $pagination->toArray();
|
||||
$pagination['data'] = $comments;
|
||||
|
||||
return $pagination;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user