feat: refactor likes extension

This commit is contained in:
Sami Mazouz 2024-03-01 21:22:07 +01:00
parent e19346efd3
commit 40d219e988
No known key found for this signature in database
7 changed files with 97 additions and 170 deletions

View File

@ -9,16 +9,16 @@
namespace Flarum\Likes;
use Flarum\Api\Controller;
use Flarum\Api\Serializer\BasicUserSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Api\Endpoint;
use Flarum\Api\Resource;
use Flarum\Extend;
use Flarum\Likes\Api\LoadLikesRelationship;
use Flarum\Likes\Api\PostResourceFields;
use Flarum\Likes\Event\PostWasLiked;
use Flarum\Likes\Event\PostWasUnliked;
use Flarum\Likes\Notification\PostLikedBlueprint;
use Flarum\Likes\Query\LikedByFilter;
use Flarum\Likes\Query\LikedFilter;
use Flarum\Post\Event\Deleted;
use Flarum\Post\Filter\PostSearcher;
use Flarum\Post\Post;
use Flarum\Search\Database\DatabaseSearchDriver;
@ -39,43 +39,28 @@ return [
new Extend\Locales(__DIR__.'/locale'),
(new Extend\Notification())
->type(PostLikedBlueprint::class, PostSerializer::class, ['alert']),
->type(PostLikedBlueprint::class, ['alert']),
(new Extend\ApiSerializer(PostSerializer::class))
->hasMany('likes', BasicUserSerializer::class)
->attribute('canLike', function (PostSerializer $serializer, $model) {
return (bool) $serializer->getActor()->can('like', $model);
})
->attribute('likesCount', function (PostSerializer $serializer, $model) {
return $model->getAttribute('likes_count') ?: 0;
(new Extend\ApiResource(Resource\PostResource::class))
->fields(PostResourceFields::class)
->endpoint(
[Endpoint\Index::class, Endpoint\Show::class, Endpoint\Create::class, Endpoint\Update::class],
function (Endpoint\Index|Endpoint\Show|Endpoint\Create|Endpoint\Update $endpoint): Endpoint\Endpoint {
return $endpoint->addDefaultInclude(['likes']);
}
),
(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())
->listen(PostWasLiked::class, Listener\SendNotificationWhenPostIsLiked::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))
->addFilter(PostSearcher::class, LikedByFilter::class)

View File

@ -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 [];
}
}

View 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);
}),
];
}
}

View File

@ -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();
}
}

View File

@ -72,7 +72,7 @@ class LikePostTest extends TestCase
$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);
}
@ -92,7 +92,7 @@ class LikePostTest extends TestCase
$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());
}

View File

@ -11,7 +11,7 @@ namespace Flarum\Likes\Tests\integration\api\discussions;
use Carbon\Carbon;
use Flarum\Group\Group;
use Flarum\Likes\Api\LoadLikesRelationship;
use Flarum\Likes\Api\PostResourceFields;
use Flarum\Testing\integration\RetrievesAuthorizedUsers;
use Flarum\Testing\integration\TestCase;
use Illuminate\Support\Arr;
@ -132,7 +132,7 @@ class ListPostsTest extends TestCase
$likes = $data['relationships']['likes']['data'];
// Only displays a limited amount of likes
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
$this->assertCount(PostResourceFields::$maxLikes, $likes);
// Displays the correct count of likes
$this->assertEquals(11, $data['attributes']['likesCount']);
// Of the limited amount of likes, the actor always appears
@ -159,7 +159,7 @@ class ListPostsTest extends TestCase
$likes = $data[0]['relationships']['likes']['data'];
// Only displays a limited amount of likes
$this->assertCount(LoadLikesRelationship::$maxLikes, $likes);
$this->assertCount(PostResourceFields::$maxLikes, $likes);
// Displays the correct count of likes
$this->assertEquals(11, $data[0]['attributes']['likesCount']);
// Of the limited amount of likes, the actor always appears
@ -170,7 +170,7 @@ class ListPostsTest extends TestCase
* @dataProvider likesIncludeProvider
* @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
$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)
->where('type', 'posts')
->where('id', 101)
->first()['relationships']['likes']['data'];
->first()['relationships']['likes']['data'] ?? null;
// 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
$this->assertEquals(11, collect($included)
->where('type', 'posts')
->where('id', 101)
->first()['attributes']['likesCount']);
->first()['attributes']['likesCount'] ?? null, $body);
// 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
@ -204,7 +209,7 @@ class ListPostsTest extends TestCase
return [
['posts,posts.likes'],
['posts.likes'],
[''],
[null],
];
}
}

View File

@ -2,9 +2,8 @@
namespace Flarum\Mentions\Api;
use Flarum\Api\Context;
use Flarum\Api\Schema;
use Flarum\Post\Post;
use Illuminate\Database\Eloquent\Builder;
class PostResourceFields
{
@ -19,7 +18,7 @@ class PostResourceFields
Schema\Relationship\ToMany::make('mentionedBy')
->type('posts')
->includable()
->limit(static::$maxMentionedBy),
->constrain(fn (Builder $query) => $query->oldest('id')->limit(static::$maxMentionedBy)),
Schema\Relationship\ToMany::make('mentionsPosts')
->type('posts'),
Schema\Relationship\ToMany::make('mentionsUsers')