1
0
mirror of https://github.com/flarum/core.git synced 2025-10-18 18:26:07 +02:00

Massive refactor

- Use contextual namespaces within Flarum\Core
- Clean up and docblock everything
- Refactor Activity/Notification blueprint stuff
- Refactor Formatter stuff
- Refactor Search stuff
- Upgrade to JSON-API 1.0
- Removed “addedPosts” and “removedPosts” relationships from discussion
API. This was used for adding/removing event posts after renaming a
discussion etc. Instead we should make an additional request to get all
new posts

Todo:
- Fix Extenders and extensions
- Get rid of repository interfaces
- Fix other bugs I’ve inevitably introduced
This commit is contained in:
Toby Zerner
2015-07-04 12:24:48 +09:30
parent 12dd550a14
commit a74b40fe47
324 changed files with 6443 additions and 4197 deletions

View File

@@ -1,42 +1,33 @@
<?php namespace Flarum\Core\Notifications;
interface NotificationInterface
/**
* A notification Blueprint, when instantiated, represents a notification about
* something. The blueprint is used by the NotificationSyncer to commit the
* notification to the database.
*/
interface Blueprint
{
/**
* Get the model that is the subject of this activity.
*
* @return \Flarum\Core\Models\Model
*/
public function getSubject();
/**
* Get the user that sent the notification.
*
* @return \Flarum\Core\Models\User|null
* @return \Flarum\Core\Users\User|null
*/
public function getSender();
/**
* Get the model that is the subject of this activity.
*
* @return \Flarum\Core\Model|null
*/
public function getSubject();
/**
* Get the data to be stored in the notification.
*
* @return array
* @return array|null
*/
public function getData();
/**
* Get the name of the view to construct a notification email with.
*
* @return string
*/
public function getEmailView();
/**
* Get the subject line for a notification email.
*
* @return string
*/
public function getEmailSubject();
/**
* Get the serialized type of this activity.
*
@@ -50,11 +41,4 @@ interface NotificationInterface
* @return string
*/
public static function getSubjectModel();
/**
* Whether or not the notification is able to be sent as an email.
*
* @return boolean
*/
public static function isEmailable();
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Notifications\Commands;
use Flarum\Core\Users\User;
class ReadNotification
{
/**
* The ID of the notification to mark as read.
*
* @var int
*/
public $notificationId;
/**
* The user performing the action.
*
* @var User
*/
public $actor;
/**
* @param int $notificationId The ID of the notification to mark as read.
* @param User $actor The user performing the action.
*/
public function __construct($notificationId, User $actor)
{
$this->notificationId = $notificationId;
$this->actor = $actor;
}
}

View File

@@ -0,0 +1,33 @@
<?php namespace Flarum\Core\Notifications\Commands;
use Flarum\Core\Notifications\Notification;
use Flarum\Core\Exceptions\PermissionDeniedException;
use Flarum\Core\Support\DispatchesEvents;
class ReadNotificationHandler
{
use DispatchesEvents;
/**
* @param ReadNotification $command
* @return Notification
* @throws \Flarum\Core\Exceptions\PermissionDeniedException
*/
public function handle(ReadNotification $command)
{
$actor = $command->actor;
if ($actor->isGuest()) {
throw new PermissionDeniedException;
}
$notification = Notification::where('user_id', $actor->id)->findOrFail($command->notificationId);
$notification->read();
$notification->save();
$this->dispatchEventsFor($notification);
return $notification;
}
}

View File

@@ -1,36 +1,57 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Models\DiscussionRenamedPost;
use Flarum\Core\Posts\DiscussionRenamedPost;
class DiscussionRenamedNotification extends NotificationAbstract
class DiscussionRenamedBlueprint implements Blueprint
{
/**
* @var DiscussionRenamedPost
*/
protected $post;
/**
* @param DiscussionRenamedPost $post
*/
public function __construct(DiscussionRenamedPost $post)
{
$this->post = $post;
}
public function getSubject()
{
return $this->post->discussion;
}
/**
* {@inheritdoc}
*/
public function getSender()
{
return $this->post->user;
}
/**
* {@inheritdoc}
*/
public function getSubject()
{
return $this->post->discussion;
}
/**
* {@inheritdoc}
*/
public function getData()
{
return ['postNumber' => (int) $this->post->number];
}
/**
* {@inheritdoc}
*/
public static function getType()
{
return 'discussionRenamed';
}
/**
* {@inheritdoc}
*/
public static function getSubjectModel()
{
return 'Flarum\Core\Models\Discussion';

View File

@@ -0,0 +1,31 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Users\User;
class EloquentNotificationRepository implements NotificationRepositoryInterface
{
/**
* {@inheritdoc}
*/
public function findByUser(User $user, $limit = null, $offset = 0)
{
$primaries = Notification::select(
app('flarum.db')->raw('MAX(id) AS id'),
app('flarum.db')->raw('SUM(is_read = 0) AS unread_count')
)
->where('user_id', $user->id)
->whereIn('type', $user->getAlertableNotificationTypes())
->where('is_deleted', false)
->groupBy('type', 'subject_id')
->latest('time')
->skip($offset)
->take($limit);
return Notification::with('subject')
->select('notifications.*', 'p.unread_count')
->mergeBindings($primaries->getQuery())
->join(app('flarum.db')->raw('('.$primaries->toSql().') p'), 'notifications.id', '=', 'p.id')
->latest('time')
->get();
}
}

View File

@@ -0,0 +1,30 @@
<?php namespace Flarum\Core\Notifications\Events;
use Flarum\Core\Notifications\Blueprint;
class NotificationWillBeSent
{
/**
* The blueprint for the notification.
*
* @var Blueprint
*/
public $blueprint;
/**
* The users that the notification will be sent to.
*
* @var array
*/
public $users;
/**
* @param Blueprint $blueprint
* @param \Flarum\Core\Users\User[] $users
*/
public function __construct(Blueprint $blueprint, array &$users)
{
$this->blueprint = $blueprint;
$this->users = $users;
}
}

View File

@@ -0,0 +1,56 @@
<?php namespace Flarum\Core\Notifications\Listeners;
use Flarum\Core\Discussions\Events\DiscussionWasRenamed;
use Flarum\Core\Posts\DiscussionRenamedPost;
use Flarum\Core\Notifications\DiscussionRenamedBlueprint;
use Flarum\Core\Notifications\NotificationSyncer;
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(DiscussionWasRenamed::class, __CLASS__.'@whenDiscussionWasRenamed');
}
/**
* @param DiscussionWasRenamed $event
*/
public function whenDiscussionWasRenamed(DiscussionWasRenamed $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,18 @@
<?php namespace Flarum\Core\Notifications;
interface MailableBlueprint
{
/**
* Get the name of the view to construct a notification email with.
*
* @return string
*/
public function getEmailView();
/**
* Get the subject line for a notification email.
*
* @return string
*/
public function getEmailSubject();
}

View File

@@ -0,0 +1,137 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Model;
/**
* Models a notification record in the database.
*
* A notification record is associated with a user, and shows up in their
* notification list. A notification indicates that something has happened that
* the user should know about, like if a user's discussion was renamed by
* someone else.
*
* Each notification record has a *type*. The type determines how the record
* looks in the notifications list, and what *subject* is associated with it.
* For example, the 'discussionRenamed' notification type represents that
* someone renamed a user's discussion. Its subject is a discussion, of which
* the ID is stored in the `subject_id` column.
*/
class Notification extends Model
{
/**
* {@inheritdoc}
*/
protected $table = 'notifications';
/**
* {@inheritdoc}
*/
protected static $dateAttributes = ['time'];
/**
* A map of notification types and the model classes to use for their
* subjects. For example, the 'discussionRenamed' notification type, which
* represents that a user's discussion was renamed, has the subject model
* class 'Flarum\Core\Discussions\Discussion'.
*
* @var array
*/
protected static $subjectModels = [];
/**
* Mark a notification as read.
*
* @return void
*/
public function read()
{
$this->is_read = true;
}
/**
* When getting the data attribute, unserialize the JSON stored in the
* database into a plain array.
*
* @param string $value
* @return mixed
*/
public function getDataAttribute($value)
{
return json_decode($value, true);
}
/**
* When setting the data attribute, serialize it into JSON for storage in
* the database.
*
* @param mixed $value
*/
public function setDataAttribute($value)
{
$this->attributes['data'] = json_encode($value);
}
/**
* Get the subject model for this notification record by looking up its
* type in our subject model map.
*
* @return string|null
*/
public function getSubjectModelAttribute()
{
return array_get(static::$subjectModels, $this->type);
}
/**
* Define the relationship with the notification's recipient.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function user()
{
return $this->belongsTo('Flarum\Core\Users\User', 'user_id');
}
/**
* Define the relationship with the notification's sender.
*
* @return \Illuminate\Database\Eloquent\Relations\BelongsTo
*/
public function sender()
{
return $this->belongsTo('Flarum\Core\Users\User', 'sender_id');
}
/**
* Define the relationship with the notification's subject.
*
* @return \Illuminate\Database\Eloquent\Relations\MorphTo
*/
public function subject()
{
return $this->morphTo('subject', 'subjectModel', 'subject_id');
}
/**
* Get the type-to-subject-model map.
*
* @return array
*/
public static function getSubjectModels()
{
return static::$subjectModels;
}
/**
* Set the subject model for the given notification type.
*
* @param string $type The notification type.
* @param string $subjectModel The class name of the subject model for that
* type.
* @return void
*/
public static function setSubjectModel($type, $subjectModel)
{
static::$subjectModels[$type] = $subjectModel;
}
}

View File

@@ -1,54 +0,0 @@
<?php namespace Flarum\Core\Notifications;
abstract class NotificationAbstract implements NotificationInterface
{
/**
* Get the user that sent the notification.
*
* @return \Flarum\Core\Models\User|null
*/
public function getSender()
{
return null;
}
/**
* Get the data to be stored in the notification.
*
* @return array
*/
public function getData()
{
return null;
}
/**
* Get the name of the view to construct a notification email with.
*
* @return string
*/
public function getEmailView()
{
return '';
}
/**
* Get the subject line for a notification email.
*
* @return string
*/
public function getEmailSubject()
{
return '';
}
/**
* Whether or not the notification is able to be sent as an email.
*
* @return boolean
*/
public static function isEmailable()
{
return false;
}
}

View File

@@ -1,25 +1,36 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Models\User;
use Flarum\Core\Models\Forum;
use Flarum\Core\Users\User;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Mail\Message;
class NotificationMailer
{
public function __construct(Mailer $mailer, Forum $forum)
/**
* @var Mailer
*/
protected $mailer;
/**
* @param Mailer $mailer
*/
public function __construct(Mailer $mailer)
{
$this->mailer = $mailer;
$this->forum = $forum;
}
public function send(NotificationInterface $notification, User $user)
/**
* @param MailableBlueprint $blueprint
* @param User $user
*/
public function send(MailableBlueprint $blueprint, User $user)
{
$this->mailer->send(
$notification->getEmailView(),
$blueprint->getEmailView(),
compact('notification', 'user'),
function ($message) use ($notification, $user) {
function (Message $message) use ($blueprint, $user) {
$message->to($user->email, $user->username)
->subject($notification->getEmailSubject());
->subject($blueprint->getEmailSubject());
}
);
}

View File

@@ -0,0 +1,16 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Users\User;
interface NotificationRepositoryInterface
{
/**
* Find a user's notifications.
*
* @param User $user
* @param int|null $count
* @param int $start
* @return \Illuminate\Database\Eloquent\Collection
*/
public function findByUser(User $user, $count = null, $start = 0);
}

View File

@@ -1,23 +1,49 @@
<?php namespace Flarum\Core\Notifications;
use Flarum\Core\Repositories\NotificationRepositoryInterface;
use Flarum\Core\Models\Notification;
use Flarum\Core\Events\NotificationWillBeSent;
use Flarum\Core\Notifications\Events\NotificationWillBeSent;
use Flarum\Core\Users\User;
use Carbon\Carbon;
use Closure;
/**
* The Notification Syncer commits notification blueprints to the database, and
* sends them via email depending on user preference. Where a blueprint
* represents a single notification, the syncer associates it with a particular
* user(s) and makes it available in their inbox.
*/
class NotificationSyncer
{
/**
* Whether or not notifications are being limited to one per user.
*
* @var bool
*/
protected $onePerUser = false;
/**
* An internal list of user IDs that notifications have been sent to.
*
* @var int[]
*/
protected $sentTo = [];
/**
* @var NotificationRepositoryInterface
*/
protected $notifications;
/**
* @var NotificationMailer
*/
protected $mailer;
public function __construct(NotificationRepositoryInterface $notifications, NotificationMailer $mailer)
{
/**
* @param NotificationRepositoryInterface $notifications
* @param NotificationMailer $mailer
*/
public function __construct(
NotificationRepositoryInterface $notifications,
NotificationMailer $mailer
) {
$this->notifications = $notifications;
$this->mailer = $mailer;
}
@@ -27,68 +53,88 @@ class NotificationSyncer
* visible to anyone else. If it is being made visible for the first time,
* attempt to send the user an email.
*
* @param \Flarum\Core\Notifications\NotificationInterface $notification
* @param \Flarum\Core\Models\User[] $users
* @param Blueprint $blueprint
* @param User[] $users
* @return void
*/
public function sync(NotificationInterface $notification, array $users)
public function sync(Blueprint $blueprint, array $users)
{
$attributes = $this->getAttributes($notification);
$attributes = $this->getAttributes($blueprint);
// Find all existing notification records in the database matching this
// blueprint. We will begin by assuming that they all need to be
// deleted in order to match the provided list of users.
$toDelete = Notification::where($attributes)->get();
$toUndelete = [];
$newRecipients = [];
// For each of the provided users, check to see if they already have
// a notification record in the database. If they do, we will make sure
// it isn't marked as deleted. If they don't, we will want to create a
// new record for them.
foreach ($users as $user) {
$existing = $toDelete->where('user_id', $user->id)->first();
$existing = $toDelete->first(function ($i, $notification) use ($user) {
return $notification->user_id === $user->id;
});
if (($k = $toDelete->search($existing)) !== false) {
if ($existing) {
$toUndelete[] = $existing->id;
$toDelete->pull($k);
$toDelete->forget($toDelete->search($existing));
} elseif (! $this->onePerUser || ! in_array($user->id, $this->sentTo)) {
$newRecipients[] = $user;
$this->sentTo[] = $user->id;
}
}
// Delete all of the remaining notification records which weren't
// removed from this collection by the above loop. Un-delete the
// existing records that we want to keep.
if (count($toDelete)) {
Notification::whereIn('id', $toDelete->lists('id'))->update(['is_deleted' => true]);
$this->setDeleted($toDelete->lists('id'), true);
}
if (count($toUndelete)) {
Notification::whereIn('id', $toUndelete)->update(['is_deleted' => false]);
$this->setDeleted($toUndelete, false);
}
// Create a notification record, and send an email, for all users
// receiving this notification for the first time (we know because they
// didn't have a record in the database).
if (count($newRecipients)) {
$now = Carbon::now('utc')->toDateTimeString();
event(new NotificationWillBeSent($notification, $newRecipients));
Notification::insert(
array_map(function ($user) use ($attributes, $notification, $now) {
return $attributes + ['user_id' => $user->id, 'time' => $now];
}, $newRecipients)
);
foreach ($newRecipients as $user) {
if ($user->shouldEmail($notification::getType())) {
$this->mailer->send($notification, $user);
}
}
$this->sendNotifications($blueprint, $newRecipients);
}
}
public function delete(NotificationInterface $notification)
/**
* Delete a notification for all users.
*
* @param Blueprint $blueprint
* @return void
*/
public function delete(Blueprint $blueprint)
{
Notification::where($this->getAttributes($notification))->update(['is_deleted' => true]);
Notification::where($this->getAttributes($blueprint))->update(['is_deleted' => true]);
}
public function restore(NotificationInterface $notification)
/**
* Restore a notification for all users.
*
* @param Blueprint $blueprint
* @return void
*/
public function restore(Blueprint $blueprint)
{
Notification::where($this->getAttributes($notification))->update(['is_deleted' => false]);
Notification::where($this->getAttributes($blueprint))->update(['is_deleted' => false]);
}
public function onePerUser(Closure $callback)
/**
* Limit notifications to one per user for the entire duration of the given
* callback.
*
* @param callable $callback
* @return void
*/
public function onePerUser(callable $callback)
{
$this->sentTo = [];
$this->onePerUser = true;
@@ -98,13 +144,75 @@ class NotificationSyncer
$this->onePerUser = false;
}
protected function getAttributes(NotificationInterface $notification)
/**
* Create a notification record and send an email (depending on user
* preference) from a blueprint to a list of recipients.
*
* @param Blueprint $blueprint
* @param User[] $recipients
*/
protected function sendNotifications(Blueprint $blueprint, array $recipients)
{
$now = Carbon::now('utc')->toDateTimeString();
event(new NotificationWillBeSent($blueprint, $recipients));
$attributes = $this->getAttributes($blueprint);
Notification::insert(
array_map(function (User $user) use ($attributes, $now) {
return $attributes + [
'user_id' => $user->id,
'time' => $now
];
}, $recipients)
);
if ($blueprint instanceof MailableBlueprint) {
$this->mailNotifications($blueprint);
}
}
/**
* Mail a notification to a list of users.
*
* @param MailableBlueprint $blueprint
* @param User[] $recipients
*/
protected function mailNotifications(MailableBlueprint $blueprint, array $recipients)
{
foreach ($recipients as $user) {
if ($user->shouldEmail($blueprint::getType())) {
$this->mailer->send($blueprint, $user);
}
}
}
/**
* Set the deleted status of a list of notification records.
*
* @param int[] $ids
* @param bool $isDeleted
*/
protected function setDeleted(array $ids, $isDeleted)
{
Notification::whereIn('id', $ids)->update(['is_deleted' => $isDeleted]);
}
/**
* Construct an array of attributes to be stored in a notification record in
* the database, given a notification blueprint.
*
* @param Blueprint $blueprint
* @return array
*/
protected function getAttributes(Blueprint $blueprint)
{
return [
'type' => $notification::getType(),
'sender_id' => $notification->getSender()->id,
'subject_id' => $notification->getSubject()->id,
'data' => ($data = $notification->getData()) ? json_encode($data) : null
'type' => $blueprint::getType(),
'sender_id' => ($sender = $blueprint->getSender()) ? $sender->id : null,
'subject_id' => ($subject = $blueprint->getSubject()) ? $subject->id : null,
'data' => ($data = $blueprint->getData()) ? json_encode($data) : null
];
}
}

View File

@@ -13,19 +13,24 @@ class NotificationsServiceProvider extends ServiceProvider
public function boot()
{
$this->extend([
(new Extend\EventSubscriber('Flarum\Core\Handlers\Events\DiscussionRenamedNotifier')),
(new Extend\EventSubscriber('Flarum\Core\Notifications\Listeners\DiscussionRenamedNotifier')),
(new Extend\NotificationType('Flarum\Core\Notifications\DiscussionRenamedNotification'))
(new Extend\NotificationType('Flarum\Core\Notifications\DiscussionRenamedBlueprint'))
->subjectSerializer('Flarum\Api\Serializers\DiscussionBasicSerializer')
->enableByDefault('alert')
]);
}
/**
* Register the service provider.
*
* @return void
*/
public function register()
{
$this->app->bind(
'Flarum\Core\Repositories\NotificationRepositoryInterface',
'Flarum\Core\Repositories\EloquentNotificationRepository'
'Flarum\Core\Notifications\NotificationRepositoryInterface',
'Flarum\Core\Notifications\EloquentNotificationRepository'
);
}
}