Passed
Pull Request — main (#100)
by Tan
03:49
created

PostNavigationService::getRelatedPostByRelevance()   B

Complexity

Conditions 9
Paths 8

Size

Total Lines 81
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 9
eloc 43
c 2
b 0
f 1
nc 8
nop 2
dl 0
loc 81
rs 7.6764

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace CSlant\Blog\Api\Services;
4
5
use Botble\Blog\Repositories\Interfaces\PostInterface;
0 ignored issues
show
Bug introduced by
The type Botble\Blog\Repositories\Interfaces\PostInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
6
use CSlant\Blog\Core\Http\Responses\Base\BaseHttpResponse;
7
8
/**
9
 * Class PostNavigationService
10
 *
11
 * @package CSlant\Blog\Api\Services
12
 *
13
 * @method BaseHttpResponse httpResponse()
14
 */
15
class PostNavigationService
16
{
17
    public function __construct(
18
        protected PostInterface $postRepository
19
    ) {
20
    }
21
22
    /**
23
     * Get previous post based on content relevance
24
     *
25
     * @param int|string $postId
26
     * @return null|object
27
     */
28
    public function getPreviousPost(int|string $postId): ?object
29
    {
30
        return $this->getRelatedPostByRelevance($postId, 'previous');
31
    }
32
33
    /**
34
     * Get next post based on content relevance
35
     *
36
     * @param int|string $postId
37
     * @return null|object
38
     */
39
    public function getNextPost(int|string $postId): ?object
40
    {
41
        return $this->getRelatedPostByRelevance($postId, 'next');
42
    }
43
44
    /**
45
     * Get both previous and next posts
46
     *
47
     * @param int|string $postId
48
     * @return array{previous: null|object, next: null|object}
49
     */
50
    public function getNavigatePosts(int|string $postId): array
51
    {
52
        return [
53
            'previous' => $this->getPreviousPost($postId),
54
            'next' => $this->getNextPost($postId),
55
        ];
56
    }
57
58
    /**
59
     * Get posts with relevance score based on shared categories and tags
60
     * Navigation is purely content-based, not time-based
61
     *
62
     * @param int|string $postId
63
     * @param string $direction 'previous' or 'next'
64
     * @return null|object
65
     */
66
    protected function getRelatedPostByRelevance(int|string $postId, string $direction = 'previous'): ?object
67
    {
68
        $currentPost = $this->postRepository->findById($postId);
69
70
        if (!$currentPost) {
71
            return null;
72
        }
73
74
        // Load current post's categories and tags
75
        $currentPost->load(['categories', 'tags']);
76
        $categoryIds = $currentPost->categories->pluck('id')->toArray();
77
        $tagIds = $currentPost->tags->pluck('id')->toArray();
78
79
        if (empty($categoryIds) && empty($tagIds)) {
80
            // No categories or tags, return null (no navigation)
81
            return null;
82
        }
83
84
        // Get all published posts except current one
85
        $posts = $this->postRepository->getModel()
86
            ->wherePublished()
87
            ->where('id', '!=', $postId)
88
            ->with(['slugable', 'categories', 'tags', 'author'])
89
            ->get();
90
91
        if ($posts->isEmpty()) {
92
            return null;
93
        }
94
95
        // Calculate relevance score for each post
96
        $scoredPosts = $posts->map(function ($post) use ($categoryIds, $tagIds) {
97
            $post->load(['categories', 'tags']);
98
99
            $postCategoryIds = $post->categories->pluck('id')->toArray();
100
            $postTagIds = $post->tags->pluck('id')->toArray();
101
102
            // Calculate shared categories and tags
103
            $sharedCategories = count(array_intersect($categoryIds, $postCategoryIds));
104
            $sharedTags = count(array_intersect($tagIds, $postTagIds));
105
106
            // Weight categories higher than tags
107
            $relevanceScore = ($sharedCategories * 3) + ($sharedTags * 1);
108
109
            $post->relevance_score = $relevanceScore;
110
111
            return $post;
112
        });
113
114
        // Filter posts with relevance score > 0
115
        $relevantPosts = $scoredPosts
116
            ->filter(function ($post) {
117
                return $post->relevance_score > 0;
118
            })
119
            ->sortByDesc('relevance_score')
120
            ->values(); // Reset array keys
121
122
        if ($relevantPosts->isEmpty()) {
123
            return null;
124
        }
125
126
        // Group posts by relevance score
127
        $groupedByScore = $relevantPosts->groupBy('relevance_score');
128
        $scores = $groupedByScore->keys()->sortDesc();
129
130
        if ($direction === 'previous') {
131
            // Get highest scoring post(s), pick first one
132
            $highestScorePosts = $groupedByScore->get($scores->first());
133
134
            return $highestScorePosts->first();
135
        } else {
136
            // For 'next', try to get a different post
137
            if ($scores->count() > 1) {
138
                // If we have multiple score levels, get from second highest
139
                $secondHighestPosts = $groupedByScore->get($scores->get(1));
140
141
                return $secondHighestPosts->first();
142
            } else {
143
                // If all posts have same score, get second post if available
144
                $highestScorePosts = $groupedByScore->get($scores->first());
145
146
                return $highestScorePosts->count() > 1 ? $highestScorePosts->get(1) : $highestScorePosts->first();
147
            }
148
        }
149
    }
150
}
151