Skip to content

Commit

Permalink
* merge trait PostWithIsMatchQuery into Post
Browse files Browse the repository at this point in the history
+ interface `SortablePost` as a contract of g/setter of props `isMatchQuery` & `sortingKey` in trait `Post` and let all classes that using this trait also implement it
+ prop `replies` and its g/setter @ `Thread`
+ prop `subReplies` and its g/setter @ `Reply`
@ `App\DTO\Post`

* migrate from the `Collection` of normalized DTO array to DTO and `SortablePost` itself
- param `$shouldRemoveSortingKey` and var `$removeSortingKey` as `SortablePost->getSortingKey()` marked as `#[Symfony\Component\Serializer\Attribute\Ignore]`
@ `reOrderNestedPosts()`

* now will set children posts as the value of nested posts prop without normalizing DTO to array @ `nestPostsWithParent()`
@ `App\PostsQuery\PostsTree`
@ be
  • Loading branch information
n0099 committed Oct 26, 2024
1 parent be0d48b commit a6c25e2
Show file tree
Hide file tree
Showing 7 changed files with 114 additions and 83 deletions.
25 changes: 24 additions & 1 deletion be/src/DTO/Post/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,34 @@
namespace App\DTO\Post;

use App\DTO\TimestampedDTO;
use Symfony\Component\Serializer\Attribute\Ignore;

trait Post
{
use TimestampedDTO { fromEntity as private fromTimestampedEntity; }

Check failure on line 10 in be/src/DTO/Post/Post.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Post.php: PSR12.Traits.UseDeclaration.SpaceAfterOpeningBrace: First trait conflict resolution statement must be on the line after the opening brace

Check failure on line 10 in be/src/DTO/Post/Post.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Post.php: PSR12.Traits.UseDeclaration.SpaceBeforeClosingBrace: Closing brace must be on the line after the last trait conflict resolution statement
use PostWithIsMatchQuery;

private bool $isMatchQuery;
private mixed $sortingKey = null;

public function getIsMatchQuery(): bool
{
return $this->isMatchQuery;
}

public function setIsMatchQuery(bool $isMatchQuery): void
{
$this->isMatchQuery = $isMatchQuery;
}

#[Ignore] public function getSortingKey(): mixed
{
return $this->sortingKey;
}

public function setSortingKey(mixed $sortingKey): void
{
$this->sortingKey = $sortingKey;
}

public static function fromEntity(\App\Entity\Post\Post $entity): self
{
Expand Down
18 changes: 0 additions & 18 deletions be/src/DTO/Post/PostWithIsMatchQuery.php

This file was deleted.

16 changes: 15 additions & 1 deletion be/src/DTO/Post/Reply.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,26 @@
namespace App\DTO\Post;

use App\Entity\Post\Reply as ReplyEntity;
use Illuminate\Support\Collection;

class Reply extends ReplyEntity
class Reply extends ReplyEntity implements SortablePost
{
use Post { fromEntity as private fromPostEntity; }

Check failure on line 10 in be/src/DTO/Post/Reply.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Reply.php: PSR12.Traits.UseDeclaration.SpaceAfterOpeningBrace: First trait conflict resolution statement must be on the line after the opening brace

Check failure on line 10 in be/src/DTO/Post/Reply.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Reply.php: PSR12.Traits.UseDeclaration.SpaceBeforeClosingBrace: Closing brace must be on the line after the last trait conflict resolution statement
use PostWithContent;

private Collection $subReplies;

public function getSubReplies(): Collection
{
return $this->subReplies;
}

public function setSubReplies(Collection $subReplies): self
{
$this->subReplies = $subReplies;
return $this;
}

public static function fromEntity(ReplyEntity $entity): self
{
$dto = self::fromPostEntity($entity);
Expand Down
16 changes: 16 additions & 0 deletions be/src/DTO/Post/SortablePost.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace App\DTO\Post;

use Symfony\Component\Serializer\Attribute\Ignore;

interface SortablePost
{
public function getIsMatchQuery(): bool;

public function setIsMatchQuery(bool $isMatchQuery): void;

#[Ignore] public function getSortingKey(): mixed;

public function setSortingKey(mixed $sortingKey): void;
}
2 changes: 1 addition & 1 deletion be/src/DTO/Post/SubReply.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
use App\Entity\Post\SubReply as SubReplyEntity;
use Symfony\Component\Serializer\Attribute\Ignore;

class SubReply extends SubReplyEntity
class SubReply extends SubReplyEntity implements SortablePost
{
use Post { fromEntity as private fromPostEntity; }

Check failure on line 10 in be/src/DTO/Post/SubReply.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

SubReply.php: PSR12.Traits.UseDeclaration.SpaceAfterOpeningBrace: First trait conflict resolution statement must be on the line after the opening brace

Check failure on line 10 in be/src/DTO/Post/SubReply.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

SubReply.php: PSR12.Traits.UseDeclaration.SpaceBeforeClosingBrace: Closing brace must be on the line after the last trait conflict resolution statement
use PostWithContent;
Expand Down
16 changes: 15 additions & 1 deletion be/src/DTO/Post/Thread.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,25 @@
namespace App\DTO\Post;

use App\Entity\Post\Thread as ThreadEntity;
use Illuminate\Support\Collection;

class Thread extends ThreadEntity
class Thread extends ThreadEntity implements SortablePost
{
use Post { fromEntity as private fromPostEntity; }

Check failure on line 10 in be/src/DTO/Post/Thread.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Thread.php: PSR12.Traits.UseDeclaration.SpaceAfterOpeningBrace: First trait conflict resolution statement must be on the line after the opening brace

Check failure on line 10 in be/src/DTO/Post/Thread.php

View workflow job for this annotation

GitHub Actions / runs-on (windows-latest) / phpcs

Thread.php: PSR12.Traits.UseDeclaration.SpaceBeforeClosingBrace: Closing brace must be on the line after the last trait conflict resolution statement

private Collection $replies;

public function getReplies(): Collection
{
return $this->replies;
}

public function setReplies(Collection $replies): self
{
$this->replies = $replies;
return $this;
}

public static function fromEntity(ThreadEntity $entity): self
{
$dto = self::fromPostEntity($entity);
Expand Down
104 changes: 43 additions & 61 deletions be/src/PostsQuery/PostsTree.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace App\PostsQuery;

use App\DTO\Post\SortablePost;
use App\DTO\PostKey\Reply as ReplyKey;
use App\DTO\PostKey\SubReply as SubReplyKey;
use App\DTO\PostKey\Thread as ThreadKey;
Expand All @@ -13,7 +14,6 @@
use App\Helper;
use App\Repository\Post\PostRepositoryFactory;
use Illuminate\Support\Collection;
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
use Symfony\Component\Stopwatch\Stopwatch;

/** @psalm-import-type PostsKeyByTypePluralName from CursorCodec */
Expand All @@ -29,7 +29,6 @@
public Collection $subReplies;

public function __construct(
private NormalizerInterface $normalizer,
private Stopwatch $stopwatch,
private PostRepositoryFactory $postRepositoryFactory,
) {}
Expand Down Expand Up @@ -106,7 +105,7 @@ public function fillWithParentPost(QueryResult $result): array

/**
* @phpcs:ignore Generic.Files.LineLength.TooLong
* @return Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed|Collection<int, Collection<string, mixed>>>>>>
* @return Collection<int, Thread>
* @SuppressWarnings(PHPMD.CamelCaseParameterName)
*/
public function nestPostsWithParent(): Collection
Expand All @@ -115,113 +114,96 @@ public function nestPostsWithParent(): Collection

$replies = $this->replies->groupBy(fn(Reply $reply) => $reply->getTid());
$subReplies = $this->subReplies->groupBy(fn(SubReply $subReply) => $subReply->getPid());
$ret = $this->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();
$ret = $this->threads->map(fn(Thread $thread) =>
$thread->setReplies($replies
->get($thread->getTid(), collect())
->map(fn(Reply $reply) =>
$reply->setSubReplies($subReplies->get($reply->getPid(), collect())))
));

$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
* @param Collection<int, Thread> $nestedPosts
* @return list<array<string, mixed|list<array<string, mixed|list<array<string, mixed>>>>>>
*/
public function reOrderNestedPosts(
Collection $nestedPosts,
string $orderByField,
bool $orderByDesc,
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>>>>
* @template T of Thread|Reply
* @param T $curPost
* @param Collection<(T is Thread ? Reply : (T is Reply ? SubReply : never))> $childPosts
* @return T
*/
$setSortingKeyFromCurrentAndChildPosts = static function (
Collection $curPost,
string $childPostTypePluralName,
) use ($orderByField, $orderByDesc): Collection {
/** @var Collection<int, Collection<string, mixed>> $childPosts sorted child posts */
$childPosts = $curPost[$childPostTypePluralName];
$curPost[$childPostTypePluralName] = $childPosts->values(); // reset keys

Thread|Reply $curPost,
Collection $childPosts,
) use ($orderByField, $orderByDesc): Thread|Reply {
// use the topmost value between sorting key or value of orderBy field within its child posts
/* @var ?(T is Thread ? Reply : (T is Reply ? SubReply : never)) $firstChildPost */
$firstChildPost = $childPosts->first();
$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)
->filter(static fn(SortablePost $p) => $p->getIsMatchQuery() === true)
// if no child posts matching the query, use null as the sorting key
->first()[$orderByField] ?? null,
->first()
?->{"get$orderByField"}(),
// 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,
$firstChildPost?->getSortingKey(),
]);
if ($curPost['isMatchQuery'] === true) {
if ($curPost->getIsMatchQuery() === true) {
// also try to use the value of orderBy field in current post
$curAndChildSortingKeys->push($curPost[$orderByField]);
$curAndChildSortingKeys->push($curPost->{"get$orderByField"}());
}

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

return $curPost;
};
$sortBySortingKey = static fn(Collection $posts): Collection => $posts
->sortBy(fn(Collection $i) => $i['sortingKey'], descending: $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(
->sortBy(fn(SortablePost $post) => $post->getSortingKey(), descending: $orderByDesc);
$ret = $sortBySortingKey(
$nestedPosts->map(
/**
* @param Collection{replies: Collection} $thread
* @return Collection{replies: Collection}
*/
function (Collection $thread) use (
function (Thread $thread) use (
$orderByField,
$orderByDesc,
$sortBySortingKey,
$removeSortingKey,
$setSortingKeyFromCurrentAndChildPosts
) {
$thread['replies'] = $sortBySortingKey($thread['replies']->map(
/**
* @param Collection{subReplies: Collection} $reply
* @return Collection{subReplies: Collection}
*/
function (Collection $reply) use ($orderByField, $orderByDesc, $setSortingKeyFromCurrentAndChildPosts) {
$reply['subReplies'] = $reply['subReplies']->sortBy(
fn(Collection $subReplies) => $subReplies->get($orderByField),
): Thread {
$thread->setReplies($sortBySortingKey($thread->getReplies()->map(
function (Reply $reply) use (
$orderByField,
$orderByDesc,
$setSortingKeyFromCurrentAndChildPosts
): Reply {
$reply->setSubReplies($reply->getSubReplies()->sortBy(
fn(SubReply $subReplies) => $subReplies->{"get$orderByField"}(),
descending: $orderByDesc,
);
return $setSortingKeyFromCurrentAndChildPosts($reply, 'subReplies');
));
return $setSortingKeyFromCurrentAndChildPosts($reply, $reply->getSubReplies());
},
));
$setSortingKeyFromCurrentAndChildPosts($thread, 'replies');
$thread['replies'] = $removeSortingKey($thread['replies']);
)));
$setSortingKeyFromCurrentAndChildPosts($thread, $thread->getReplies());
return $thread;
},
),
))->values()->toArray();
)->values()->toArray();

$this->stopwatch->stop('reOrderNestedPosts');
return $ret;
Expand Down

0 comments on commit a6c25e2

Please sign in to comment.