Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
49 / 49 |
|
100.00% |
6 / 6 |
CRAP | |
100.00% |
1 / 1 |
RelatedPublicationsComponent | |
100.00% |
49 / 49 |
|
100.00% |
6 / 6 |
17 | |
100.00% |
1 / 1 |
__construct | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
render | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
makeRelatedPublications | |
100.00% |
20 / 20 |
|
100.00% |
1 / 1 |
6 | |||
getTagsForPage | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getAllRelatedPages | |
100.00% |
13 / 13 |
|
100.00% |
1 / 1 |
3 | |||
sortRelatedPagesByRelevance | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
4 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Hyde\Publications\Views\Components; |
6 | |
7 | use Hyde\Hyde; |
8 | use Hyde\Publications\Concerns\PublicationFieldTypes; |
9 | use Hyde\Publications\Models\PublicationFieldDefinition; |
10 | use Hyde\Publications\Pages\PublicationPage; |
11 | use Hyde\Publications\Publications; |
12 | use Illuminate\Contracts\View\View; |
13 | use Illuminate\Support\Collection; |
14 | use Illuminate\View\Component; |
15 | |
16 | use function collect; |
17 | use function count; |
18 | use function view; |
19 | |
20 | class RelatedPublicationsComponent extends Component |
21 | { |
22 | /** @var Collection<string, PublicationPage> */ |
23 | public Collection $relatedPublications; |
24 | |
25 | public string $title = 'Related Publications'; |
26 | |
27 | public function __construct(string $title = 'Related Publications', int $limit = 5) |
28 | { |
29 | $this->relatedPublications = collect($this->makeRelatedPublications($limit)); |
30 | $this->title = $title; |
31 | } |
32 | |
33 | /** @interitDoc */ |
34 | public function render(): View |
35 | { |
36 | return view('hyde-publications::components.related-publications'); |
37 | } |
38 | |
39 | protected function makeRelatedPublications(int $limit = 5): array |
40 | { |
41 | // Get current publicationType from the current page |
42 | $currentHydePage = Hyde::currentRoute()->getPage(); |
43 | |
44 | // If not a publication page, exit early |
45 | if (! $currentHydePage instanceof PublicationPage) { |
46 | return []; |
47 | } |
48 | |
49 | $publicationType = $currentHydePage->getType(); |
50 | |
51 | // Get the tag fields for the current publicationType or exit early if there aren't any |
52 | $publicationTypeTagFields = $publicationType->getFields()->filter(function (PublicationFieldDefinition $field): bool { |
53 | return $field->type === PublicationFieldTypes::Tag; |
54 | }); |
55 | if ($publicationTypeTagFields->isEmpty()) { |
56 | return []; |
57 | } |
58 | |
59 | // Get a list of all pages for this page's publicationType: 1 means we only have current page & no related pages exist |
60 | $publicationPages = Publications::getPublicationsForType($publicationType)->keyBy('identifier'); |
61 | if ($publicationPages->count() <= 1) { |
62 | return []; |
63 | } |
64 | |
65 | // Get all tags for the current page |
66 | $currentPageTags = $this->getTagsForPage($publicationPages->get($currentHydePage->getIdentifier()), $publicationTypeTagFields); |
67 | if ($currentPageTags->isEmpty()) { |
68 | return []; |
69 | } |
70 | |
71 | // Forget the current page pages since we don't want to show it as a related page against itself |
72 | $publicationPages->forget($currentHydePage->getIdentifier()); |
73 | |
74 | // Get all related pages |
75 | $allRelatedPages = $this->getAllRelatedPages($publicationPages, $publicationTypeTagFields, $currentPageTags); |
76 | if ($allRelatedPages->isEmpty()) { |
77 | return []; |
78 | } |
79 | |
80 | // Sort them by relevance (count of shared tags & newest dates) |
81 | return $this->sortRelatedPagesByRelevance($allRelatedPages, $limit)->all(); |
82 | } |
83 | |
84 | protected function getTagsForPage(PublicationPage $publicationPage, Collection $tagFields): Collection |
85 | { |
86 | $thisPageTags = collect(); |
87 | |
88 | // There could be multiple tag fields, but most publication types will only have one |
89 | foreach ($tagFields as $tagField) { |
90 | $thisPageTags = $thisPageTags->merge($publicationPage->matter->get($tagField->name, [])); |
91 | } |
92 | |
93 | return $thisPageTags; |
94 | } |
95 | |
96 | protected function getAllRelatedPages(Collection $publicationPages, Collection $tagFields, Collection $currPageTags): Collection |
97 | { |
98 | $allRelatedPages = collect(); |
99 | |
100 | foreach ($publicationPages as $publicationPage) { |
101 | $publicationPageTags = $this->getTagsForPage($publicationPage, $tagFields); |
102 | $matchedTagCount = $publicationPageTags->intersect($currPageTags)->count(); |
103 | |
104 | // We have shared/matching tags, add this page info to $allRelatedPages |
105 | if ($matchedTagCount) { |
106 | $allRelatedPages->add( |
107 | collect([ |
108 | 'count' => $matchedTagCount, |
109 | 'identifier' => $publicationPage->identifier, |
110 | 'page' => $publicationPage, |
111 | ]) |
112 | ); |
113 | } |
114 | } |
115 | |
116 | return $allRelatedPages; |
117 | } |
118 | |
119 | protected function sortRelatedPagesByRelevance(Collection $allRelatedPages, int $max): Collection |
120 | { |
121 | $relatedPages = collect(); |
122 | |
123 | // Group related pages by the number of shared tags and then sort by keys (number of shared tags) descending |
124 | $allRelatedPagesGrouped = $allRelatedPages->groupBy('count')->sortKeysDesc(SORT_NUMERIC); |
125 | |
126 | // Iterate over groups |
127 | foreach ($allRelatedPagesGrouped as $relatedPagesGroup) { |
128 | // Sort group by recency, with the latest pages first |
129 | $sortedPageGroup = $relatedPagesGroup->sortByDesc('page.matter.__createdAt'); |
130 | |
131 | // Now add to $relatedPages, and stop when hitting $max |
132 | foreach ($sortedPageGroup as $page) { |
133 | $relatedPages->put($page['identifier'], $page['page']); |
134 | if (count($relatedPages) >= $max) { |
135 | break 2; |
136 | } |
137 | } |
138 | } |
139 | |
140 | return $relatedPages; |
141 | } |
142 | } |