mirror of
https://github.com/flarum/framework.git
synced 2024-12-11 13:05:50 +08:00
feat: refactor likes extension
This commit is contained in:
parent
e19346efd3
commit
40d219e988
|
@ -9,16 +9,16 @@
|
||||||
|
|
||||||
namespace Flarum\Likes;
|
namespace Flarum\Likes;
|
||||||
|
|
||||||
use Flarum\Api\Controller;
|
use Flarum\Api\Endpoint;
|
||||||
use Flarum\Api\Serializer\BasicUserSerializer;
|
use Flarum\Api\Resource;
|
||||||
use Flarum\Api\Serializer\PostSerializer;
|
|
||||||
use Flarum\Extend;
|
use Flarum\Extend;
|
||||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
use Flarum\Likes\Api\PostResourceFields;
|
||||||
use Flarum\Likes\Event\PostWasLiked;
|
use Flarum\Likes\Event\PostWasLiked;
|
||||||
use Flarum\Likes\Event\PostWasUnliked;
|
use Flarum\Likes\Event\PostWasUnliked;
|
||||||
use Flarum\Likes\Notification\PostLikedBlueprint;
|
use Flarum\Likes\Notification\PostLikedBlueprint;
|
||||||
use Flarum\Likes\Query\LikedByFilter;
|
use Flarum\Likes\Query\LikedByFilter;
|
||||||
use Flarum\Likes\Query\LikedFilter;
|
use Flarum\Likes\Query\LikedFilter;
|
||||||
|
use Flarum\Post\Event\Deleted;
|
||||||
use Flarum\Post\Filter\PostSearcher;
|
use Flarum\Post\Filter\PostSearcher;
|
||||||
use Flarum\Post\Post;
|
use Flarum\Post\Post;
|
||||||
use Flarum\Search\Database\DatabaseSearchDriver;
|
use Flarum\Search\Database\DatabaseSearchDriver;
|
||||||
|
@ -39,43 +39,28 @@ return [
|
||||||
new Extend\Locales(__DIR__.'/locale'),
|
new Extend\Locales(__DIR__.'/locale'),
|
||||||
|
|
||||||
(new Extend\Notification())
|
(new Extend\Notification())
|
||||||
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
|
->type(PostLikedBlueprint::class, ['alert']),
|
||||||
|
|
||||||
(new Extend\ApiSerializer(PostSerializer::class))
|
(new Extend\ApiResource(Resource\PostResource::class))
|
||||||
->hasMany('likes', BasicUserSerializer::class)
|
->fields(PostResourceFields::class)
|
||||||
->attribute('canLike', function (PostSerializer $serializer, $model) {
|
->endpoint(
|
||||||
return (bool) $serializer->getActor()->can('like', $model);
|
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
|
||||||
})
|
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
|
||||||
->attribute('likesCount', function (PostSerializer $serializer, $model) {
|
return $endpoint->addDefaultInclude(['likes']);
|
||||||
return $model->getAttribute('likes_count') ?: 0;
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
(new Extend\ApiResource(Resource\DiscussionResource::class))
|
||||||
|
->endpoint(Endpoint\Show::class, function (Endpoint\Show $endpoint): Endpoint\Endpoint {
|
||||||
|
return $endpoint->addDefaultInclude(['posts.likes']);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
(new Extend\ApiController(Controller\ShowDiscussionController::class))
|
|
||||||
->addInclude('posts.likes')
|
|
||||||
->loadWhere('posts.likes', LoadLikesRelationship::mutateRelation(...))
|
|
||||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
|
||||||
|
|
||||||
(new Extend\ApiController(Controller\ListPostsController::class))
|
|
||||||
->addInclude('likes')
|
|
||||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
|
||||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
|
||||||
(new Extend\ApiController(Controller\ShowPostController::class))
|
|
||||||
->addInclude('likes')
|
|
||||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
|
||||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
|
||||||
(new Extend\ApiController(Controller\CreatePostController::class))
|
|
||||||
->addInclude('likes')
|
|
||||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
|
||||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
|
||||||
(new Extend\ApiController(Controller\UpdatePostController::class))
|
|
||||||
->addInclude('likes')
|
|
||||||
->loadWhere('likes', LoadLikesRelationship::mutateRelation(...))
|
|
||||||
->prepareDataForSerialization(LoadLikesRelationship::countRelation(...)),
|
|
||||||
|
|
||||||
(new Extend\Event())
|
(new Extend\Event())
|
||||||
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::class)
|
||||||
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
->listen(PostWasUnliked::class, Listener\SendNotificationWhenPostIsUnliked::class)
|
||||||
->subscribe(Listener\SaveLikesToDatabase::class),
|
->listen(Deleted::class, function (Deleted $event) {
|
||||||
|
$event->post->likes()->detach();
|
||||||
|
}),
|
||||||
|
|
||||||
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
(new Extend\SearchDriver(DatabaseSearchDriver::class))
|
||||||
->addFilter(PostSearcher::class, LikedByFilter::class)
|
->addFilter(PostSearcher::class, LikedByFilter::class)
|
||||||
|
|
|
@ -1,65 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* For detailed copyright and license information, please view the
|
|
||||||
* LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Likes\Api;
|
|
||||||
|
|
||||||
use Flarum\Api\Controller\AbstractSerializeController;
|
|
||||||
use Flarum\Discussion\Discussion;
|
|
||||||
use Flarum\Http\RequestUtil;
|
|
||||||
use Flarum\Post\Post;
|
|
||||||
use Illuminate\Database\Eloquent\Collection;
|
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
|
||||||
use Illuminate\Database\Query\Expression;
|
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
|
||||||
|
|
||||||
class LoadLikesRelationship
|
|
||||||
{
|
|
||||||
public static int $maxLikes = 4;
|
|
||||||
|
|
||||||
public static function mutateRelation(BelongsToMany $query, ServerRequestInterface $request): void
|
|
||||||
{
|
|
||||||
$actor = RequestUtil::getActor($request);
|
|
||||||
|
|
||||||
$grammar = $query->getQuery()->getGrammar();
|
|
||||||
|
|
||||||
$query
|
|
||||||
// So that we can tell if the current user has liked the post.
|
|
||||||
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
|
||||||
// Limiting a relationship results is only possible because
|
|
||||||
// the Post model uses the \Staudenmeir\EloquentEagerLimit\HasEagerLimit
|
|
||||||
// trait.
|
|
||||||
->limit(self::$maxLikes);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called using the @see ApiController::prepareDataForSerialization extender.
|
|
||||||
*/
|
|
||||||
public static function countRelation(AbstractSerializeController $controller, mixed $data): array
|
|
||||||
{
|
|
||||||
$loadable = null;
|
|
||||||
|
|
||||||
if ($data instanceof Discussion) {
|
|
||||||
// We do this because the ShowDiscussionController manipulates the posts
|
|
||||||
// in a way that some of them are just ids.
|
|
||||||
$loadable = $data->posts->filter(function ($post) {
|
|
||||||
return $post instanceof Post;
|
|
||||||
});
|
|
||||||
} elseif ($data instanceof Collection) {
|
|
||||||
$loadable = $data;
|
|
||||||
} elseif ($data instanceof Post) {
|
|
||||||
$loadable = $data->newCollection([$data]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($loadable) {
|
|
||||||
$loadable->loadCount('likes');
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
|
58
extensions/likes/src/Api/PostResourceFields.php
Normal file
58
extensions/likes/src/Api/PostResourceFields.php
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Flarum\Likes\Api;
|
||||||
|
|
||||||
|
use Flarum\Api\Context;
|
||||||
|
use Flarum\Api\Schema;
|
||||||
|
use Flarum\Likes\Event\PostWasLiked;
|
||||||
|
use Flarum\Likes\Event\PostWasUnliked;
|
||||||
|
use Flarum\Post\Post;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
use Illuminate\Database\Query\Expression;
|
||||||
|
|
||||||
|
class PostResourceFields
|
||||||
|
{
|
||||||
|
public static int $maxLikes = 4;
|
||||||
|
|
||||||
|
public function __invoke(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
Schema\Boolean::make('isLiked')
|
||||||
|
->visible(false)
|
||||||
|
->writable(fn (Post $post, Context $context) => $context->getActor()->can('like', $post))
|
||||||
|
->set(function (Post $post, bool $liked, Context $context) {
|
||||||
|
$actor = $context->getActor();
|
||||||
|
|
||||||
|
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
||||||
|
|
||||||
|
if ($liked && ! $currentlyLiked) {
|
||||||
|
$post->likes()->attach($actor->id);
|
||||||
|
|
||||||
|
$post->raise(new PostWasLiked($post, $actor));
|
||||||
|
} elseif ($currentlyLiked) {
|
||||||
|
$post->likes()->detach($actor->id);
|
||||||
|
|
||||||
|
$post->raise(new PostWasUnliked($post, $actor));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
Schema\Boolean::make('canLike')
|
||||||
|
->get(fn (Post $post, Context $context) => $context->getActor()->can('like', $post)),
|
||||||
|
Schema\Integer::make('likesCount')
|
||||||
|
->countRelation('likes'),
|
||||||
|
|
||||||
|
Schema\Relationship\ToMany::make('likes')
|
||||||
|
->type('users')
|
||||||
|
->includable()
|
||||||
|
->constrain(function (Builder $query, Context $context) {
|
||||||
|
$actor = $context->getActor();
|
||||||
|
$grammar = $query->getQuery()->getGrammar();
|
||||||
|
|
||||||
|
// So that we can tell if the current user has liked the post.
|
||||||
|
$query
|
||||||
|
->orderBy(new Expression($grammar->wrap('user_id').' = '.$actor->id), 'desc')
|
||||||
|
->limit(static::$maxLikes);
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,55 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
/*
|
|
||||||
* This file is part of Flarum.
|
|
||||||
*
|
|
||||||
* For detailed copyright and license information, please view the
|
|
||||||
* LICENSE file that was distributed with this source code.
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Flarum\Likes\Listener;
|
|
||||||
|
|
||||||
use Flarum\Likes\Event\PostWasLiked;
|
|
||||||
use Flarum\Likes\Event\PostWasUnliked;
|
|
||||||
use Flarum\Post\Event\Deleted;
|
|
||||||
use Flarum\Post\Event\Saving;
|
|
||||||
use Illuminate\Contracts\Events\Dispatcher;
|
|
||||||
|
|
||||||
class SaveLikesToDatabase
|
|
||||||
{
|
|
||||||
public function subscribe(Dispatcher $events): void
|
|
||||||
{
|
|
||||||
$events->listen(Saving::class, $this->whenPostIsSaving(...));
|
|
||||||
$events->listen(Deleted::class, $this->whenPostIsDeleted(...));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostIsSaving(Saving $event): void
|
|
||||||
{
|
|
||||||
$post = $event->post;
|
|
||||||
$data = $event->data;
|
|
||||||
|
|
||||||
if ($post->exists && isset($data['attributes']['isLiked'])) {
|
|
||||||
$actor = $event->actor;
|
|
||||||
$liked = (bool) $data['attributes']['isLiked'];
|
|
||||||
|
|
||||||
$actor->assertCan('like', $post);
|
|
||||||
|
|
||||||
$currentlyLiked = $post->likes()->where('user_id', $actor->id)->exists();
|
|
||||||
|
|
||||||
if ($liked && ! $currentlyLiked) {
|
|
||||||
$post->likes()->attach($actor->id);
|
|
||||||
|
|
||||||
$post->raise(new PostWasLiked($post, $actor));
|
|
||||||
} elseif ($currentlyLiked) {
|
|
||||||
$post->likes()->detach($actor->id);
|
|
||||||
|
|
||||||
$post->raise(new PostWasUnliked($post, $actor));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function whenPostIsDeleted(Deleted $event): void
|
|
||||||
{
|
|
||||||
$event->post->likes()->detach();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -72,7 +72,7 @@ class LikePostTest extends TestCase
|
||||||
|
|
||||||
$post = CommentPost::query()->find($postId);
|
$post = CommentPost::query()->find($postId);
|
||||||
|
|
||||||
$this->assertEquals(200, $response->getStatusCode());
|
$this->assertEquals(200, $response->getStatusCode(), $response->getBody()->getContents());
|
||||||
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
$this->assertNotNull($post->likes->where('id', $authenticatedAs)->first(), $message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,7 +92,7 @@ class LikePostTest extends TestCase
|
||||||
|
|
||||||
$post = CommentPost::query()->find($postId);
|
$post = CommentPost::query()->find($postId);
|
||||||
|
|
||||||
$this->assertEquals(403, $response->getStatusCode(), $message);
|
$this->assertContainsEquals($response->getStatusCode(), [401, 403], $message);
|
||||||
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
$this->assertNull($post->likes->where('id', $authenticatedAs)->first());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ namespace Flarum\Likes\Tests\integration\api\discussions;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
use Flarum\Group\Group;
|
use Flarum\Group\Group;
|
||||||
use Flarum\Likes\Api\LoadLikesRelationship;
|
use Flarum\Likes\Api\PostResourceFields;
|
||||||
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
|
||||||
use Flarum\Testing\integration\TestCase;
|
use Flarum\Testing\integration\TestCase;
|
||||||
use Illuminate\Support\Arr;
|
use Illuminate\Support\Arr;
|
||||||
|
@ -132,7 +132,7 @@ class ListPostsTest extends TestCase
|
||||||
$likes = $data['relationships']['likes']['data'];
|
$likes = $data['relationships']['likes']['data'];
|
||||||
|
|
||||||
// Only displays a limited amount of likes
|
// Only displays a limited amount of likes
|
||||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||||
// Displays the correct count of likes
|
// Displays the correct count of likes
|
||||||
$this->assertEquals(11, $data['attributes']['likesCount']);
|
$this->assertEquals(11, $data['attributes']['likesCount']);
|
||||||
// Of the limited amount of likes, the actor always appears
|
// Of the limited amount of likes, the actor always appears
|
||||||
|
@ -159,7 +159,7 @@ class ListPostsTest extends TestCase
|
||||||
$likes = $data[0]['relationships']['likes']['data'];
|
$likes = $data[0]['relationships']['likes']['data'];
|
||||||
|
|
||||||
// Only displays a limited amount of likes
|
// Only displays a limited amount of likes
|
||||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||||
// Displays the correct count of likes
|
// Displays the correct count of likes
|
||||||
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
|
||||||
// Of the limited amount of likes, the actor always appears
|
// Of the limited amount of likes, the actor always appears
|
||||||
|
@ -170,7 +170,7 @@ class ListPostsTest extends TestCase
|
||||||
* @dataProvider likesIncludeProvider
|
* @dataProvider likesIncludeProvider
|
||||||
* @test
|
* @test
|
||||||
*/
|
*/
|
||||||
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(string $include)
|
public function likes_relation_returns_limited_results_and_shows_only_visible_posts_in_show_discussion_endpoint(?string $include)
|
||||||
{
|
{
|
||||||
// Show discussion endpoint
|
// Show discussion endpoint
|
||||||
$response = $this->send(
|
$response = $this->send(
|
||||||
|
@ -181,22 +181,27 @@ class ListPostsTest extends TestCase
|
||||||
])
|
])
|
||||||
);
|
);
|
||||||
|
|
||||||
$included = json_decode($response->getBody()->getContents(), true)['included'];
|
$body = $response->getBody()->getContents();
|
||||||
|
|
||||||
|
$this->assertEquals(200, $response->getStatusCode(), $body);
|
||||||
|
|
||||||
|
$included = json_decode($body, true)['included'] ?? [];
|
||||||
|
|
||||||
$likes = collect($included)
|
$likes = collect($included)
|
||||||
->where('type', 'posts')
|
->where('type', 'posts')
|
||||||
->where('id', 101)
|
->where('id', 101)
|
||||||
->first()['relationships']['likes']['data'];
|
->first()['relationships']['likes']['data'] ?? null;
|
||||||
|
|
||||||
// Only displays a limited amount of likes
|
// Only displays a limited amount of likes
|
||||||
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
|
$this->assertNotNull($likes, $body);
|
||||||
|
$this->assertCount(PostResourceFields::$maxLikes, $likes);
|
||||||
// Displays the correct count of likes
|
// Displays the correct count of likes
|
||||||
$this->assertEquals(11, collect($included)
|
$this->assertEquals(11, collect($included)
|
||||||
->where('type', 'posts')
|
->where('type', 'posts')
|
||||||
->where('id', 101)
|
->where('id', 101)
|
||||||
->first()['attributes']['likesCount']);
|
->first()['attributes']['likesCount'] ?? null, $body);
|
||||||
// Of the limited amount of likes, the actor always appears
|
// Of the limited amount of likes, the actor always appears
|
||||||
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'));
|
$this->assertEquals([2, 102, 104, 105], Arr::pluck($likes, 'id'), $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function likesIncludeProvider(): array
|
public function likesIncludeProvider(): array
|
||||||
|
@ -204,7 +209,7 @@ class ListPostsTest extends TestCase
|
||||||
return [
|
return [
|
||||||
['posts,posts.likes'],
|
['posts,posts.likes'],
|
||||||
['posts.likes'],
|
['posts.likes'],
|
||||||
[''],
|
[null],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,8 @@
|
||||||
|
|
||||||
namespace Flarum\Mentions\Api;
|
namespace Flarum\Mentions\Api;
|
||||||
|
|
||||||
use Flarum\Api\Context;
|
|
||||||
use Flarum\Api\Schema;
|
use Flarum\Api\Schema;
|
||||||
use Flarum\Post\Post;
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
class PostResourceFields
|
class PostResourceFields
|
||||||
{
|
{
|
||||||
|
@ -19,7 +18,7 @@ class PostResourceFields
|
||||||
Schema\Relationship\ToMany::make('mentionedBy')
|
Schema\Relationship\ToMany::make('mentionedBy')
|
||||||
->type('posts')
|
->type('posts')
|
||||||
->includable()
|
->includable()
|
||||||
->limit(static::$maxMentionedBy),
|
->constrain(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
|
||||||
Schema\Relationship\ToMany::make('mentionsPosts')
|
Schema\Relationship\ToMany::make('mentionsPosts')
|
||||||
->type('posts'),
|
->type('posts'),
|
||||||
Schema\Relationship\ToMany::make('mentionsUsers')
|
Schema\Relationship\ToMany::make('mentionsUsers')
|
||||||
|
|
Loading…
Reference in New Issue
Block a user