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; 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)

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

View File

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

View File

@ -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')