1
0
mirror of https://github.com/flarum/core.git synced 2025-08-09 01:46:35 +02:00

Extract new Flarum\Discussion namespace

This commit is contained in:
Franz Liedke
2017-06-24 13:48:04 +02:00
parent 66abd7ecfd
commit 4fb38d6458
46 changed files with 149 additions and 147 deletions

View File

@@ -0,0 +1,464 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Flarum\Core\DiscussionState;
use Flarum\Post\MergeableInterface;
use Flarum\Foundation\EventGeneratorTrait;
use Flarum\Database\ScopeVisibilityTrait;
use Flarum\Database\AbstractModel;
use Flarum\Discussion\Event\Deleted;
use Flarum\Discussion\Event\Hidden;
use Flarum\Discussion\Event\Renamed;
use Flarum\Discussion\Event\Restored;
use Flarum\Discussion\Event\Started;
use Flarum\Post\Event\Deleted;
use Flarum\Event\ScopePostVisibility;
use Flarum\Post\Post;
use Flarum\User\Guest;
use Flarum\User\User;
use Flarum\Util\Str;
/**
* @property int $id
* @property string $title
* @property string $slug
* @property int $comments_count
* @property int $participants_count
* @property int $number_index
* @property \Carbon\Carbon $start_time
* @property int|null $start_user_id
* @property int|null $start_post_id
* @property \Carbon\Carbon|null $last_time
* @property int|null $last_user_id
* @property int|null $last_post_id
* @property int|null $last_post_number
* @property \Carbon\Carbon|null $hide_time
* @property int|null $hide_user_id
* @property DiscussionState|null $state
* @property \Illuminate\Database\Eloquent\Collection $posts
* @property \Illuminate\Database\Eloquent\Collection $comments
* @property \Illuminate\Database\Eloquent\Collection $participants
* @property Post|null $startPost
* @property User|null $startUser
* @property Post|null $lastPost
* @property User|null $lastUser
* @property \Illuminate\Database\Eloquent\Collection $readers
* @property bool $is_private
*/
class Discussion extends AbstractModel
{
use EventGeneratorTrait;
use ScopeVisibilityTrait;
/**
* {@inheritdoc}
*/
protected $table = 'discussions';
/**
* An array of posts that have been modified during this request.
*
* @var array
*/
protected $modifiedPosts = [];
/**
* {@inheritdoc}
*/
protected $dates = ['start_time', 'last_time', 'hide_time'];
/**
* Casts properties to a specific type.
*
* @var array
*/
protected $casts = [
'is_private' => 'boolean'
];
/**
* The user for which the state relationship should be loaded.
*
* @var User
*/
protected static $stateUser;
/**
* Boot the model.
*
* @return void
*/
public static function boot()
{
parent::boot();
static::deleted(function ($discussion) {
$discussion->raise(new Deleted($discussion));
// Delete all of the posts in the discussion. Before we delete them
// in a big batch query, we will loop through them and raise a
// PostWasDeleted event for each post.
$posts = $discussion->posts()->allTypes();
foreach ($posts->get() as $post) {
$discussion->raise(new Deleted($post));
}
$posts->delete();
// Delete all of the 'state' records for all of the users who have
// read the discussion.
$discussion->readers()->detach();
});
}
/**
* Start a new discussion. Raises the DiscussionWasStarted event.
*
* @param string $title
* @param User $user
* @return static
*/
public static function start($title, User $user)
{
$discussion = new static;
$discussion->title = $title;
$discussion->start_time = time();
$discussion->start_user_id = $user->id;
$discussion->setRelation('startUser', $user);
$discussion->raise(new Started($discussion));
return $discussion;
}
/**
* Rename the discussion. Raises the DiscussionWasRenamed event.
*
* @param string $title
* @return $this
*/
public function rename($title)
{
if ($this->title !== $title) {
$oldTitle = $this->title;
$this->title = $title;
$this->raise(new Renamed($this, $oldTitle));
}
return $this;
}
/**
* Hide the discussion.
*
* @param User $actor
* @return $this
*/
public function hide(User $actor = null)
{
if (! $this->hide_time) {
$this->hide_time = time();
$this->hide_user_id = $actor ? $actor->id : null;
$this->raise(new Hidden($this));
}
return $this;
}
/**
* Restore the discussion.
*
* @return $this
*/
public function restore()
{
if ($this->hide_time !== null) {
$this->hide_time = null;
$this->hide_user_id = null;
$this->raise(new Restored($this));
}
return $this;
}
/**
* Set the discussion's start post details.
*
* @param Post $post
* @return $this
*/
public function setStartPost(Post $post)
{
$this->start_time = $post->time;
$this->start_user_id = $post->user_id;
$this->start_post_id = $post->id;
return $this;
}
/**
* Set the discussion's last post details.
*
* @param Post $post
* @return $this
*/
public function setLastPost(Post $post)
{
$this->last_time = $post->time;
$this->last_user_id = $post->user_id;
$this->last_post_id = $post->id;
$this->last_post_number = $post->number;
return $this;
}
/**
* Refresh a discussion's last post details.
*
* @return $this
*/
public function refreshLastPost()
{
if ($lastPost = $this->comments()->latest('time')->first()) {
$this->setLastPost($lastPost);
}
return $this;
}
/**
* Refresh the discussion's comments count.
*
* @return $this
*/
public function refreshCommentsCount()
{
$this->comments_count = $this->comments()->count();
return $this;
}
/**
* Refresh the discussion's participants count.
*
* @return $this
*/
public function refreshParticipantsCount()
{
$this->participants_count = $this->participants()->count('users.id');
return $this;
}
/**
* Save a post, attempting to merge it with the discussion's last post.
*
* The merge logic is delegated to the new post. (As an example, a
* DiscussionRenamedPost will merge if adjacent to another
* DiscussionRenamedPost, and delete if the title has been reverted
* completely.)
*
* @param \Flarum\Post\MergeableInterface $post The post to save.
* @return Post The resulting post. It may or may not be the same post as
* was originally intended to be saved. It also may not exist, if the
* merge logic resulted in deletion.
*/
public function mergePost(MergeableInterface $post)
{
$lastPost = $this->posts()->latest('time')->first();
$post = $post->saveAfter($lastPost);
return $this->modifiedPosts[] = $post;
}
/**
* Get the posts that have been modified during this request.
*
* @return array
*/
public function getModifiedPosts()
{
return $this->modifiedPosts;
}
/**
* Define the relationship with the discussion's posts.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function posts()
{
return $this->hasMany('Flarum\Post\Post');
}
/**
* Define the relationship with the discussion's posts, but only ones which
* are visible to the given user.
*
* @param User $user
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function postsVisibleTo(User $user)
{
$relation = $this->posts();
static::$dispatcher->fire(
new ScopePostVisibility($this, $relation->getQuery(), $user)
);
return $relation;
}
/**
* Define the relationship with the discussion's publicly-visible comments.
*
* @return \Illuminate\Database\Eloquent\Relations\HasMany
*/
public function comments()
{
return $this->postsVisibleTo(new Guest)->where('type', 'comment');
}
/**
* Query the discussion's participants (a list of unique users who have
* posted in the discussion).
*
* @return \Illuminate\Database\Eloquent\Builder
*/
public function participants()
{
return User::join('posts', 'posts.user_id', '=', 'users.id')
->where('posts.discussion_id', $this->id)
->select('users.*')
->distinct();
}
/**
* Define the relationship with the discussion's first post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function startPost()
{
return $this->belongsTo('Flarum\Post\Post', 'start_post_id');
}
/**
* Define the relationship with the discussion's author.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function startUser()
{
return $this->belongsTo('Flarum\User\User', 'start_user_id');
}
/**
* Define the relationship with the discussion's last post.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function lastPost()
{
return $this->belongsTo('Flarum\Post\Post', 'last_post_id');
}
/**
* Define the relationship with the discussion's most recent author.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function lastUser()
{
return $this->belongsTo('Flarum\User\User', 'last_user_id');
}
/**
* Define the relationship with the discussion's readers.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function readers()
{
return $this->belongsToMany('Flarum\User\User', 'users_discussions');
}
/**
* Define the relationship with the discussion's state for a particular
* user.
*
* If no user is passed (i.e. in the case of eager loading the 'state'
* relation), then the static `$stateUser` property is used.
*
* @see Discussion::setStateUser()
*
* @param User|null $user
* @return \Illuminate\Database\Eloquent\Relations\HasOne
*/
public function state(User $user = null)
{
$user = $user ?: static::$stateUser;
return $this->hasOne('Flarum\Core\DiscussionState')->where('user_id', $user ? $user->id : null);
}
/**
* Get the state model for a user, or instantiate a new one if it does not
* exist.
*
* @param User $user
* @return \Flarum\Core\DiscussionState
*/
public function stateFor(User $user)
{
$state = $this->state($user)->first();
if (! $state) {
$state = new DiscussionState;
$state->discussion_id = $this->id;
$state->user_id = $user->id;
}
return $state;
}
/**
* Set the user for which the state relationship should be loaded.
*
* @param User $user
*/
public static function setStateUser(User $user)
{
static::$stateUser = $user;
}
/**
* Set the discussion title.
*
* This automatically creates a matching slug for the discussion.
*
* @param string $title
*/
protected function setTitleAttribute($title)
{
$this->attributes['title'] = $title;
$this->slug = Str::slug($title);
}
}

View File

@@ -0,0 +1,104 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Flarum\Post\Post;
use Flarum\Post\Event\Deleted;
use Flarum\Post\Event\Hidden;
use Flarum\Post\Event\Posted;
use Flarum\Post\Event\Restored;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionMetadataUpdater
{
/**
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen(Posted::class, [$this, 'whenPostWasPosted']);
$events->listen(Deleted::class, [$this, 'whenPostWasDeleted']);
$events->listen(Hidden::class, [$this, 'whenPostWasHidden']);
$events->listen(Restored::class, [$this, 'whenPostWasRestored']);
}
/**
* @param Posted $event
*/
public function whenPostWasPosted(Posted $event)
{
$discussion = $event->post->discussion;
if ($discussion && $discussion->exists) {
$discussion->refreshCommentsCount();
$discussion->refreshLastPost();
$discussion->refreshParticipantsCount();
$discussion->save();
}
}
/**
* @param \Flarum\Post\Event\Deleted $event
*/
public function whenPostWasDeleted(Deleted $event)
{
$this->removePost($event->post);
$discussion = $event->post->discussion;
if ($discussion && $discussion->posts()->count() === 0) {
$discussion->delete();
}
}
/**
* @param \Flarum\Post\Event\Hidden $event
*/
public function whenPostWasHidden(Hidden $event)
{
$this->removePost($event->post);
}
/**
* @param Restored $event
*/
public function whenPostWasRestored(Restored $event)
{
$discussion = $event->post->discussion;
if ($discussion && $discussion->exists) {
$discussion->refreshCommentsCount();
$discussion->refreshParticipantsCount();
$discussion->refreshLastPost();
$discussion->save();
}
}
/**
* @param Post $post
*/
protected function removePost(Post $post)
{
$discussion = $post->discussion;
if ($discussion && $discussion->exists) {
$discussion->refreshCommentsCount();
$discussion->refreshParticipantsCount();
if ($discussion->last_post_id == $post->id) {
$discussion->refreshLastPost();
}
$discussion->save();
}
}
}

View File

@@ -0,0 +1,130 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Carbon\Carbon;
use Flarum\Discussion\Discussion;
use Flarum\Event\ScopeHiddenDiscussionVisibility;
use Flarum\Event\ScopePrivateDiscussionVisibility;
use Flarum\Settings\SettingsRepositoryInterface;
use Flarum\User\AbstractPolicy;
use Flarum\User\Gate;
use Flarum\User\User;
use Illuminate\Contracts\Events\Dispatcher;
use Illuminate\Database\Eloquent\Builder;
class DiscussionPolicy extends AbstractPolicy
{
/**
* {@inheritdoc}
*/
protected $model = Discussion::class;
/**
* @var SettingsRepositoryInterface
*/
protected $settings;
/**
* @var Gate
*/
protected $gate;
/**
* @var Dispatcher
*/
protected $events;
/**
* @param SettingsRepositoryInterface $settings
* @param Gate $gate
* @param Dispatcher $events
*/
public function __construct(SettingsRepositoryInterface $settings, Gate $gate, Dispatcher $events)
{
$this->settings = $settings;
$this->gate = $gate;
$this->events = $events;
}
/**
* @param User $actor
* @param string $ability
* @return bool|null
*/
public function after(User $actor, $ability)
{
if ($actor->hasPermission('discussion.'.$ability)) {
return true;
}
}
/**
* @param User $actor
* @param Builder $query
*/
public function find(User $actor, Builder $query)
{
// Hide private discussions per default.
$query->where(function ($query) use ($actor) {
$query->where('discussions.is_private', false);
$this->events->fire(
new ScopePrivateDiscussionVisibility($query, $actor)
);
});
if ($actor->cannot('viewDiscussions')) {
$query->whereRaw('FALSE');
} elseif (! $actor->hasPermission('discussion.hide')) {
$query->where(function ($query) use ($actor) {
$query->whereNull('discussions.hide_time')
->where('comments_count', '>', 0)
->orWhere('start_user_id', $actor->id);
$this->events->fire(
new ScopeHiddenDiscussionVisibility($query, $actor, 'discussion.hide')
);
});
}
}
/**
* @param User $actor
* @param \Flarum\Discussion\Discussion $discussion
* @return bool|null
*/
public function rename(User $actor, Discussion $discussion)
{
if ($discussion->start_user_id == $actor->id) {
$allowRenaming = $this->settings->get('allow_renaming');
if ($allowRenaming === '-1'
|| ($allowRenaming === 'reply' && $discussion->participants_count <= 1)
|| ($discussion->start_time->diffInMinutes(new Carbon) < $allowRenaming)) {
return true;
}
}
}
/**
* @param User $actor
* @param \Flarum\Discussion\Discussion $discussion
* @return bool|null
*/
public function hide(User $actor, Discussion $discussion)
{
if ($discussion->start_user_id == $actor->id && $discussion->participants_count <= 1) {
return true;
}
}
}

View File

@@ -0,0 +1,67 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Flarum\Core\Notification\DiscussionRenamedBlueprint;
use Flarum\Core\Notification\NotificationSyncer;
use Flarum\Post\DiscussionRenamedPost;
use Flarum\Discussion\Event\Renamed;
use Illuminate\Contracts\Events\Dispatcher;
class DiscussionRenamedNotifier
{
/**
* @var NotificationSyncer
*/
protected $notifications;
/**
* @param NotificationSyncer $notifications
*/
public function __construct(NotificationSyncer $notifications)
{
$this->notifications = $notifications;
}
/**
* @param Dispatcher $events
*/
public function subscribe(Dispatcher $events)
{
$events->listen(Renamed::class, [$this, 'whenDiscussionWasRenamed']);
}
/**
* @param \Flarum\Discussion\Event\Renamed $event
*/
public function whenDiscussionWasRenamed(Renamed $event)
{
$post = DiscussionRenamedPost::reply(
$event->discussion->id,
$event->actor->id,
$event->oldTitle,
$event->discussion->title
);
$post = $event->discussion->mergePost($post);
if ($event->discussion->start_user_id !== $event->actor->id) {
$blueprint = new DiscussionRenamedBlueprint($post);
if ($post->exists) {
$this->notifications->sync($blueprint, [$event->discussion->startUser]);
} else {
$this->notifications->delete($blueprint);
}
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Flarum\User\User;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Query\Expression;
class DiscussionRepository
{
/**
* Get a new query builder for the discussions table.
*
* @return Builder
*/
public function query()
{
return Discussion::query();
}
/**
* Find a discussion by ID, optionally making sure it is visible to a
* certain user, or throw an exception.
*
* @param int $id
* @param User $user
* @return \Flarum\Discussion\Discussion
*/
public function findOrFail($id, User $user = null)
{
$query = Discussion::where('id', $id);
return $this->scopeVisibleTo($query, $user)->firstOrFail();
}
/**
* Get the IDs of discussions which a user has read completely.
*
* @param User $user
* @return array
*/
public function getReadIds(User $user)
{
return Discussion::leftJoin('users_discussions', 'users_discussions.discussion_id', '=', 'discussions.id')
->where('user_id', $user->id)
->where('read_number', '>=', new Expression('last_post_number'))
->lists('id')
->all();
}
/**
* Scope a query to only include records that are visible to a user.
*
* @param Builder $query
* @param User $user
* @return Builder
*/
protected function scopeVisibleTo(Builder $query, User $user = null)
{
if ($user !== null) {
$query->whereVisibleTo($user);
}
return $query;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion;
use Flarum\Foundation\AbstractValidator;
class DiscussionValidator extends AbstractValidator
{
protected $rules = [
'title' => [
'required',
'min:3',
'max:80'
]
];
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Deleted
{
/**
* @var \Flarum\Discussion\Discussion
*/
public $discussion;
/**
* @var User
*/
public $actor;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
*/
public function __construct(Discussion $discussion, User $actor = null)
{
$this->discussion = $discussion;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Deleting
{
/**
* The discussion that is going to be deleted.
*
* @var Discussion
*/
public $discussion;
/**
* The user who is performing the action.
*
* @var User
*/
public $actor;
/**
* Any user input associated with the command.
*
* @var array
*/
public $data;
/**
* @param Discussion $discussion
* @param User $actor
* @param array $data
*/
public function __construct(Discussion $discussion, User $actor, array $data = [])
{
$this->discussion = $discussion;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Hidden
{
/**
* @var \Flarum\Discussion\Discussion
*/
public $discussion;
/**
* @var User
*/
public $actor;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
*/
public function __construct(Discussion $discussion, User $actor = null)
{
$this->discussion = $discussion;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Renamed
{
/**
* @var Discussion
*/
public $discussion;
/**
* @var string
*/
public $oldTitle;
/**
* @var User
*/
public $actor;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
* @param string $oldTitle
*/
public function __construct(Discussion $discussion, $oldTitle, User $actor = null)
{
$this->discussion = $discussion;
$this->oldTitle = $oldTitle;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Restored
{
/**
* @var \Flarum\Discussion\Discussion
*/
public $discussion;
/**
* @var User
*/
public $actor;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
*/
public function __construct(Discussion $discussion, User $actor = null)
{
$this->discussion = $discussion;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,51 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Saving
{
/**
* The discussion that will be saved.
*
* @var \Flarum\Discussion\Discussion
*/
public $discussion;
/**
* The user who is performing the action.
*
* @var User
*/
public $actor;
/**
* Any user input associated with the command.
*
* @var array
*/
public $data;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
* @param array $data
*/
public function __construct(Discussion $discussion, User $actor, array $data = [])
{
$this->discussion = $discussion;
$this->actor = $actor;
$this->data = $data;
}
}

View File

@@ -0,0 +1,38 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Discussion\Discussion;
use Flarum\User\User;
class Started
{
/**
* @var \Flarum\Discussion\Discussion
*/
public $discussion;
/**
* @var User
*/
public $actor;
/**
* @param \Flarum\Discussion\Discussion $discussion
* @param User $actor
*/
public function __construct(Discussion $discussion, User $actor = null)
{
$this->discussion = $discussion;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Core\DiscussionState;
class UserDataSaving
{
/**
* @var DiscussionState
*/
public $state;
/**
* @param DiscussionState $state
*/
public function __construct(DiscussionState $state)
{
$this->state = $state;
}
}

View File

@@ -0,0 +1,30 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Event;
use Flarum\Core\DiscussionState;
class UserRead
{
/**
* @var DiscussionState
*/
public $state;
/**
* @param DiscussionState $state
*/
public function __construct(DiscussionState $state)
{
$this->state = $state;
}
}

View File

@@ -0,0 +1,53 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search;
use Flarum\Core\Search\AbstractSearch;
/**
* An object which represents the internal state of a search for discussions:
* the search query, the user performing the search, the fallback sort order,
* relevant post information, and a log of which gambits have been used.
*/
class DiscussionSearch extends AbstractSearch
{
/**
* {@inheritdoc}
*/
protected $defaultSort = ['lastTime' => 'desc'];
/**
* @var array
*/
protected $relevantPostIds = [];
/**
* Get the related IDs for each result.
*
* @return int[]
*/
public function getRelevantPostIds()
{
return $this->relevantPostIds;
}
/**
* Set the relevant post IDs for the results.
*
* @param array $relevantPostIds
* @return void
*/
public function setRelevantPostIds(array $relevantPostIds)
{
$this->relevantPostIds = $relevantPostIds;
}
}

View File

@@ -0,0 +1,138 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search;
use Flarum\Discussion\Discussion;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Discussion\Search\DiscussionSearch;
use Flarum\Post\PostRepository;
use Flarum\Core\Search\ApplySearchParametersTrait;
use Flarum\Core\Search\GambitManager;
use Flarum\Core\Search\SearchCriteria;
use Flarum\Core\Search\SearchResults;
use Flarum\Event\ConfigureDiscussionSearch;
use Illuminate\Database\Eloquent\Collection;
/**
* Takes a DiscussionSearchCriteria object, performs a search using gambits,
* and spits out a DiscussionSearchResults object.
*/
class DiscussionSearcher
{
use ApplySearchParametersTrait;
/**
* @var GambitManager
*/
protected $gambits;
/**
* @var DiscussionRepository
*/
protected $discussions;
/**
* @var PostRepository
*/
protected $posts;
/**
* @param GambitManager $gambits
* @param DiscussionRepository $discussions
* @param PostRepository $posts
*/
public function __construct(
GambitManager $gambits,
DiscussionRepository $discussions,
PostRepository $posts
) {
$this->gambits = $gambits;
$this->discussions = $discussions;
$this->posts = $posts;
}
/**
* @param SearchCriteria $criteria
* @param int|null $limit
* @param int $offset
* @param array $load An array of relationships to load on the results.
* @return SearchResults
*/
public function search(SearchCriteria $criteria, $limit = null, $offset = 0, array $load = [])
{
$actor = $criteria->actor;
$query = $this->discussions->query()->whereVisibleTo($actor);
// Construct an object which represents this search for discussions.
// Apply gambits to it, sort, and paging criteria. Also give extensions
// an opportunity to modify it.
$search = new DiscussionSearch($query->getQuery(), $actor);
$this->gambits->apply($search, $criteria->query);
$this->applySort($search, $criteria->sort);
$this->applyOffset($search, $offset);
$this->applyLimit($search, $limit + 1);
// TODO: inject dispatcher
event(new ConfigureDiscussionSearch($search, $criteria));
// Execute the search query and retrieve the results. We get one more
// results than the user asked for, so that we can say if there are more
// results. If there are, we will get rid of that extra result.
$discussions = $query->get();
$areMoreResults = $limit > 0 && $discussions->count() > $limit;
if ($areMoreResults) {
$discussions->pop();
}
// The relevant posts relationship isn't a typical Eloquent
// relationship; rather, we need to extract that information from our
// search object. We will delegate that task and prevent Eloquent
// from trying to load it.
if (in_array('relevantPosts', $load)) {
$this->loadRelevantPosts($discussions, $search);
$load = array_diff($load, ['relevantPosts', 'relevantPosts.discussion', 'relevantPosts.user']);
}
Discussion::setStateUser($actor);
$discussions->load($load);
return new SearchResults($discussions, $areMoreResults);
}
/**
* Load relevant posts onto each discussion using information from the
* search.
*
* @param Collection $discussions
* @param DiscussionSearch $search
*/
protected function loadRelevantPosts(Collection $discussions, DiscussionSearch $search)
{
$postIds = [];
foreach ($search->getRelevantPostIds() as $relevantPostIds) {
$postIds = array_merge($postIds, array_slice($relevantPostIds, 0, 2));
}
$posts = $postIds ? $this->posts->findByIds($postIds, $search->getActor())->load('user')->all() : [];
foreach ($discussions as $discussion) {
$discussion->relevantPosts = array_filter($posts, function ($post) use ($discussion) {
return $post->discussion_id == $discussion->id;
});
}
}
}

View File

@@ -0,0 +1,24 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Fulltext;
interface DriverInterface
{
/**
* Return an array of arrays of post IDs, grouped by discussion ID, which
* match the given string.
*
* @param string $string
* @return array
*/
public function match($string);
}

View File

@@ -0,0 +1,36 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Fulltext;
use Flarum\Core\Post;
class MySqlFulltextDriver implements DriverInterface
{
/**
* {@inheritdoc}
*/
public function match($string)
{
$discussionIds = Post::where('type', 'comment')
->whereRaw('MATCH (`content`) AGAINST (? IN BOOLEAN MODE)', [$string])
->orderByRaw('MATCH (`content`) AGAINST (?) DESC', [$string])
->lists('discussion_id', 'id');
$relevantPostIds = [];
foreach ($discussionIds as $postId => $discussionId) {
$relevantPostIds[$discussionId][] = $postId;
}
return $relevantPostIds;
}
}

View File

@@ -0,0 +1,59 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Gambit;
use Flarum\User\UserRepository;
use Flarum\Core\Search\AbstractRegexGambit;
use Flarum\Core\Search\AbstractSearch;
use Flarum\Discussion\Search\DiscussionSearch;
use LogicException;
class AuthorGambit extends AbstractRegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'author:(.+)';
/**
* @var \Flarum\User\UserRepository
*/
protected $users;
/**
* @param \Flarum\User\UserRepository $users
*/
public function __construct(UserRepository $users)
{
$this->users = $users;
}
/**
* {@inheritdoc}
*/
protected function conditions(AbstractSearch $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$usernames = trim($matches[1], '"');
$usernames = explode(',', $usernames);
$ids = [];
foreach ($usernames as $username) {
$ids[] = $this->users->getIdForUsername($username);
}
$search->getQuery()->whereIn('start_user_id', $ids, 'and', $negate);
}
}

View File

@@ -0,0 +1,45 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Gambit;
use Flarum\Core\Search\AbstractRegexGambit;
use Flarum\Core\Search\AbstractSearch;
use Flarum\Discussion\Search\DiscussionSearch;
use LogicException;
class CreatedGambit extends AbstractRegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'created:(\d{4}\-\d\d\-\d\d)(\.\.(\d{4}\-\d\d\-\d\d))?';
/**
* {@inheritdoc}
*/
protected function conditions(AbstractSearch $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
// If we've just been provided with a single YYYY-MM-DD date, then find
// discussions that were started on that exact date. But if we've been
// provided with a YYYY-MM-DD..YYYY-MM-DD range, then find discussions
// that were started during that period.
if (empty($matches[3])) {
$search->getQuery()->whereDate('start_time', $negate ? '!=' : '=', $matches[1]);
} else {
$search->getQuery()->whereBetween('start_time', [$matches[1], $matches[3]], 'and', $negate);
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Gambit;
use Flarum\Core\Search\AbstractSearch;
use Flarum\Discussion\Search\DiscussionSearch;
use Flarum\Discussion\Search\Fulltext\DriverInterface;
use Flarum\Core\Search\GambitInterface;
use LogicException;
class FulltextGambit implements GambitInterface
{
/**
* @var \Flarum\Discussion\Search\Fulltext\DriverInterface
*/
protected $fulltext;
/**
* @param \Flarum\Discussion\Search\Fulltext\DriverInterface $fulltext
*/
public function __construct(DriverInterface $fulltext)
{
$this->fulltext = $fulltext;
}
/**
* {@inheritdoc}
*/
public function apply(AbstractSearch $search, $bit)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$relevantPostIds = $this->fulltext->match($bit);
$discussionIds = array_keys($relevantPostIds);
$search->setRelevantPostIds($relevantPostIds);
$search->getQuery()->whereIn('id', $discussionIds);
$search->setDefaultSort(['id' => $discussionIds]);
}
}

View File

@@ -0,0 +1,43 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Gambit;
use Flarum\Core\Search\AbstractRegexGambit;
use Flarum\Core\Search\AbstractSearch;
use Flarum\Discussion\Search\DiscussionSearch;
use LogicException;
class HiddenGambit extends AbstractRegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'is:hidden';
/**
* {@inheritdoc}
*/
protected function conditions(AbstractSearch $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$search->getQuery()->where(function ($query) use ($negate) {
if ($negate) {
$query->whereNull('hide_time')->where('comments_count', '>', 0);
} else {
$query->whereNotNull('hide_time')->orWhere('comments_count', 0);
}
});
}
}

View File

@@ -0,0 +1,63 @@
<?php
/*
* This file is part of Flarum.
*
* (c) Toby Zerner <toby.zerner@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\Discussion\Search\Gambit;
use Flarum\Discussion\DiscussionRepository;
use Flarum\Core\Search\AbstractRegexGambit;
use Flarum\Core\Search\AbstractSearch;
use Flarum\Discussion\Search\DiscussionSearch;
use LogicException;
class UnreadGambit extends AbstractRegexGambit
{
/**
* {@inheritdoc}
*/
protected $pattern = 'is:unread';
/**
* @var \Flarum\Discussion\DiscussionRepository
*/
protected $discussions;
/**
* @param \Flarum\Discussion\DiscussionRepository $discussions
*/
public function __construct(DiscussionRepository $discussions)
{
$this->discussions = $discussions;
}
/**
* {@inheritdoc}
*/
protected function conditions(AbstractSearch $search, array $matches, $negate)
{
if (! $search instanceof DiscussionSearch) {
throw new LogicException('This gambit can only be applied on a DiscussionSearch');
}
$actor = $search->getActor();
if ($actor->exists) {
$readIds = $this->discussions->getReadIds($actor);
$search->getQuery()->where(function ($query) use ($readIds, $negate, $actor) {
if (! $negate) {
$query->whereNotIn('id', $readIds)->where('last_time', '>', $actor->read_time ?: 0);
} else {
$query->whereIn('id', $readIds)->orWhere('last_time', '<=', $actor->read_time ?: 0);
}
});
}
}
}