Skip to content

Commit

Permalink
* split methods fillWithParentPost(), nestPostsWithParent() & `re…
Browse files Browse the repository at this point in the history
…OrderNestedPosts()` into a new class `PostsTree`

* mark props `$orderBy(Field|Desc)` as `public` visibility to allow `PostsController->query()` pass them as params of `PostsTree->reOrderNestedPosts()`
@ `App\PostsQuery\BaseQuery`
@ be
  • Loading branch information
n0099 committed Oct 14, 2024
1 parent bc30ffc commit 67012ba
Show file tree
Hide file tree
Showing 5 changed files with 246 additions and 231 deletions.
8 changes: 6 additions & 2 deletions be/src/Controller/PostsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public function query(Request $request): array
$query->query($params, $request->query->get('cursor'));
$this->stopwatch->stop('$queryClass->query()');
$this->stopwatch->start('fillWithParentPost');
$result = $query->fillWithParentPost();
$result = $query->postsTree->fillWithParentPost($query->queryResult);
$this->stopwatch->stop('fillWithParentPost');

$this->stopwatch->start('queryUsers');
Expand Down Expand Up @@ -118,7 +118,11 @@ public function query(Request $request): array
...Arr::except($result, ['fid', ...Helper::POST_TYPES_PLURAL]),
],
'forum' => $this->forumRepository->getForum($fid),
'threads' => $query->reOrderNestedPosts($query->nestPostsWithParent(...$result)),
'threads' => $query->postsTree->reOrderNestedPosts(
$query->postsTree->nestPostsWithParent(),
$query->orderByField,
$query->orderByDesc,
),
'users' => $users,
'latestRepliers' => $latestRepliers,
];
Expand Down
220 changes: 3 additions & 217 deletions be/src/PostsQuery/BaseQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,15 @@

namespace App\PostsQuery;

use App\Entity\Post\Content\ReplyContent;
use App\Entity\Post\Content\SubReplyContent;
use App\Entity\Post\Reply;
use App\Entity\Post\SubReply;
use App\Entity\Post\Thread;
use App\DTO\PostKey\Thread as ThreadKey;
use App\DTO\PostKey\Reply as ReplyKey;
use App\DTO\PostKey\SubReply as SubReplyKey;
use App\Helper;
use App\Repository\Post\PostRepositoryFactory;
use Illuminate\Support\Collection;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Stopwatch\Stopwatch;

abstract readonly class BaseQuery
{
protected string $orderByField;
public string $orderByField;

protected bool $orderByDesc;
public bool $orderByDesc;

public function __construct(
private NormalizerInterface $normalizer,
private Stopwatch $stopwatch,
private PostRepositoryFactory $postRepositoryFactory,
public QueryResult $queryResult,
public PostsTree $postsTree,
) {}

abstract public function query(QueryParams $params, ?string $cursor): void;
Expand All @@ -40,202 +24,4 @@ protected function setOrderByDesc(bool $orderByDesc): void
{
$this->orderByDesc = $orderByDesc;
}

/**
* @return array{
* fid: int,
* threads: Collection<int, Thread>,
* replies: Collection<int, Reply>,
* subReplies: Collection<int, SubReply>,
* matchQueryPostCount: array{thread: int, reply: int, subReply: int},
* notMatchQueryParentPostCount: array{thread: int, reply: int},
* }
*/
public function fillWithParentPost(): array
{
$result = $this->queryResult;
/** @var Collection<int> $tids */
$tids = $result->threads->map(fn(ThreadKey $postKey) => $postKey->postId);
/** @var Collection<int> $pids */
$pids = $result->replies->map(fn(ReplyKey $postKey) => $postKey->postId);
/** @var Collection<int> $spids */
$spids = $result->subReplies->map(fn(SubReplyKey $postKey) => $postKey->postId);
$postModels = $this->postRepositoryFactory->newForumPosts($result->fid);

$this->stopwatch->start('fillWithThreadsFields');
/** @var Collection<int, int> $parentThreadsID parent tid of all replies and their sub replies */
$parentThreadsID = $result->replies
->map(fn(ReplyKey $postKey) => $postKey->parentPostId)
->concat($result->subReplies->map(fn(SubReplyKey $postKey) => $postKey->tid))
->unique();
/** @var Collection<int, Thread> $threads */
$threads = collect($postModels['thread']->getPosts($parentThreadsID->concat($tids)))
->each(static fn(Thread $thread) =>
$thread->setIsMatchQuery($tids->contains($thread->getTid())));
$this->stopwatch->stop('fillWithThreadsFields');

$this->stopwatch->start('fillWithRepliesFields');
/** @var Collection<int, int> $parentRepliesID parent pid of all sub replies */
$parentRepliesID = $result->subReplies->map(fn(SubReplyKey $postKey) => $postKey->parentPostId)->unique();
$allRepliesId = $parentRepliesID->concat($pids);
/** @var Collection<int, Reply> $replies */
$replies = collect($postModels['reply']->getPosts($allRepliesId))
->each(static fn(Reply $reply) =>
$reply->setIsMatchQuery($pids->contains($reply->getPid())));
$this->stopwatch->stop('fillWithRepliesFields');

$this->stopwatch->start('fillWithSubRepliesFields');
/** @var Collection<int, SubReply> $subReplies */
$subReplies = collect($postModels['subReply']->getPosts($spids));
$this->stopwatch->stop('fillWithSubRepliesFields');

$this->stopwatch->start('parsePostContentProtoBufBytes');
// not using one-to-one association due to relying on PostRepository->getTableNameSuffix()
$replyContents = collect($this->postRepositoryFactory->newReplyContent($result->fid)->getPostsContent($allRepliesId))
->mapWithKeys(fn(ReplyContent $content) => [$content->getPid() => $content->getContent()]);
$replies->each(fn(Reply $reply) =>
$reply->setContent($replyContents->get($reply->getPid())));

$subReplyContents = collect($this->postRepositoryFactory->newSubReplyContent($result->fid)->getPostsContent($spids))
->mapWithKeys(fn(SubReplyContent $content) => [$content->getSpid() => $content->getContent()]);
$subReplies->each(fn(SubReply $subReply) =>
$subReply->setContent($subReplyContents->get($subReply->getSpid())));
$this->stopwatch->stop('parsePostContentProtoBufBytes');

return [
'fid' => $result->fid,
'matchQueryPostCount' => collect(Helper::POST_TYPES)
->combine([$tids, $pids, $spids])
->map(static fn(Collection $ids, string $type) => $ids->count()),
'notMatchQueryParentPostCount' => [
'thread' => $parentThreadsID->diff($tids)->count(),
'reply' => $parentRepliesID->diff($pids)->count(),
],
...array_combine(Helper::POST_TYPES_PLURAL, [$threads, $replies, $subReplies]),
];
}

/**
* @param Collection<int, Thread> $threads
* @param Collection<int, Reply> $replies
* @param Collection<int, SubReply> $subReplies
* @phpcs:ignore Generic.Files.LineLength.TooLong
* @return Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed>>>>>>
* @SuppressWarnings(PHPMD.CamelCaseParameterName)
*/
public function nestPostsWithParent(
Collection $threads,
Collection $replies,
Collection $subReplies,
...$_,
): Collection {
$this->stopwatch->start('nestPostsWithParent');

$replies = $replies->groupBy(fn(Reply $reply) => $reply->getTid());
$subReplies = $subReplies->groupBy(fn(SubReply $subReply) => $subReply->getPid());
$ret = $threads
->map(fn(Thread $thread) => [
...$this->normalizer->normalize($thread),
'replies' => $replies
->get($thread->getTid(), collect())
->map(fn(Reply $reply) => [
...$this->normalizer->normalize($reply),
'subReplies' => $this->normalizer->normalize($subReplies->get($reply->getPid(), collect())),
]),
])
->recursive();

$this->stopwatch->stop('nestPostsWithParent');
return $ret;
}

/**
* @phpcs:ignore Generic.Files.LineLength.TooLong
* @param Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed>>>>>> $nestedPosts
* @return list<array<string, mixed|list<array<string, mixed|list<array<string, mixed>>>>>>
*/
public function reOrderNestedPosts(Collection $nestedPosts, bool $shouldRemoveSortingKey = true): array
{
$this->stopwatch->start('reOrderNestedPosts');

/**
* @param Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed>>>> $curPost
* @param string $childPostTypePluralName
* @return Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed>>>>
*/
$setSortingKeyFromCurrentAndChildPosts = function (
Collection $curPost,
string $childPostTypePluralName,
): Collection {
/** @var Collection<int, Collection<string, mixed>> $childPosts sorted child posts */
$childPosts = $curPost[$childPostTypePluralName];
$curPost[$childPostTypePluralName] = $childPosts->values(); // reset keys

// use the topmost value between sorting key or value of orderBy field within its child posts
$curAndChildSortingKeys = collect([
// value of orderBy field in the first sorted child post that isMatchQuery after previous sorting
$childPosts // sub replies won't have isMatchQuery
->filter(static fn(Collection $p) => ($p['isMatchQuery'] ?? true) === true)
// if no child posts matching the query, use null as the sorting key
->first()[$this->orderByField] ?? null,
// sorting key from the first sorted child posts
// not requiring isMatchQuery since a child post without isMatchQuery
// might have its own child posts with isMatchQuery
// and its sortingKey would be selected from its own child posts
$childPosts->first()['sortingKey'] ?? null,
]);
if ($curPost['isMatchQuery'] === true) {
// also try to use the value of orderBy field in current post
$curAndChildSortingKeys->push($curPost[$this->orderByField]);
}

// Collection->filter() will remove falsy values like null
$curAndChildSortingKeys = $curAndChildSortingKeys->filter()->sort();
$curPost['sortingKey'] = $this->orderByDesc
? $curAndChildSortingKeys->last()
: $curAndChildSortingKeys->first();

return $curPost;
};
$sortBySortingKey = fn(Collection $posts): Collection => $posts
->sortBy(fn(Collection $i) => $i['sortingKey'], descending: $this->orderByDesc);
$removeSortingKey = $shouldRemoveSortingKey
? /** @psalm-return Collection<array-key, Collection> */
static fn(Collection $posts): Collection => $posts
->map(fn(Collection $i) => $i->except('sortingKey'))
: static fn($i) => $i;
$ret = $removeSortingKey($sortBySortingKey(
$nestedPosts->map(
/**
* @param Collection{replies: Collection} $thread
* @return Collection{replies: Collection}
*/
function (Collection $thread) use (
$sortBySortingKey,
$removeSortingKey,
$setSortingKeyFromCurrentAndChildPosts
) {
$thread['replies'] = $sortBySortingKey($thread['replies']->map(
/**
* @param Collection{subReplies: Collection} $reply
* @return Collection{subReplies: Collection}
*/
function (Collection $reply) use ($setSortingKeyFromCurrentAndChildPosts) {
$reply['subReplies'] = $reply['subReplies']->sortBy(
fn(Collection $subReplies) => $subReplies->get($this->orderByField),
descending: $this->orderByDesc,
);
return $setSortingKeyFromCurrentAndChildPosts($reply, 'subReplies');
},
));
$setSortingKeyFromCurrentAndChildPosts($thread, 'replies');
$thread['replies'] = $removeSortingKey($thread['replies']);
return $thread;
},
),
))->values()->toArray();

$this->stopwatch->stop('reOrderNestedPosts');
return $ret;
}
}
9 changes: 3 additions & 6 deletions be/src/PostsQuery/IndexQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,16 @@
use App\Helper;
use Doctrine\ORM\QueryBuilder;
use Illuminate\Support\Collection;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Stopwatch\Stopwatch;

readonly class IndexQuery extends BaseQuery
{
public function __construct(
NormalizerInterface $normalizer,
Stopwatch $stopwatch,
private PostRepositoryFactory $postRepositoryFactory,
QueryResult $queryResult,
PostsTree $postsTree,
private PostRepositoryFactory $postRepositoryFactory,
private ForumRepository $forumRepository,
) {
parent::__construct($normalizer, $stopwatch, $postRepositoryFactory, $queryResult);
parent::__construct($queryResult, $postsTree);
}

/** @SuppressWarnings(PHPMD.ElseExpression) */
Expand Down
Loading

0 comments on commit 67012ba

Please sign in to comment.