diff --git a/extensions/likes/extend.php b/extensions/likes/extend.php index 95ee5d1ef..e84f0c5a1 100644 --- a/extensions/likes/extend.php +++ b/extensions/likes/extend.php @@ -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) diff --git a/extensions/likes/src/Api/LoadLikesRelationship.php b/extensions/likes/src/Api/LoadLikesRelationship.php deleted file mode 100644 index 4f9b62619..000000000 --- a/extensions/likes/src/Api/LoadLikesRelationship.php +++ /dev/null @@ -1,65 +0,0 @@ -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 []; - } -} diff --git a/extensions/likes/src/Api/PostResourceFields.php b/extensions/likes/src/Api/PostResourceFields.php new file mode 100644 index 000000000..401406aaf --- /dev/null +++ b/extensions/likes/src/Api/PostResourceFields.php @@ -0,0 +1,58 @@ +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); + }), + ]; + } +} diff --git a/extensions/likes/src/Listener/SaveLikesToDatabase.php b/extensions/likes/src/Listener/SaveLikesToDatabase.php deleted file mode 100755 index 8be625e9b..000000000 --- a/extensions/likes/src/Listener/SaveLikesToDatabase.php +++ /dev/null @@ -1,55 +0,0 @@ -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(); - } -} diff --git a/extensions/likes/tests/integration/api/LikePostTest.php b/extensions/likes/tests/integration/api/LikePostTest.php index 4bcbe49fa..2722686ec 100644 --- a/extensions/likes/tests/integration/api/LikePostTest.php +++ b/extensions/likes/tests/integration/api/LikePostTest.php @@ -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()); } diff --git a/extensions/likes/tests/integration/api/ListPostsTest.php b/extensions/likes/tests/integration/api/ListPostsTest.php index 59a8f7a43..5ca5c8f36 100644 --- a/extensions/likes/tests/integration/api/ListPostsTest.php +++ b/extensions/likes/tests/integration/api/ListPostsTest.php @@ -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], ]; } } diff --git a/extensions/mentions/src/Api/PostResourceFields.php b/extensions/mentions/src/Api/PostResourceFields.php index be188e2e0..4177ef050 100644 --- a/extensions/mentions/src/Api/PostResourceFields.php +++ b/extensions/mentions/src/Api/PostResourceFields.php @@ -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')