Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
RelatedPublicationsComponent
100.00% covered (success)
100.00%
49 / 49
100.00% covered (success)
100.00%
6 / 6
17
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 render
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 makeRelatedPublications
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
6
 getTagsForPage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 getAllRelatedPages
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
3
 sortRelatedPagesByRelevance
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Publications\Views\Components;
6
7use Hyde\Hyde;
8use Hyde\Publications\Concerns\PublicationFieldTypes;
9use Hyde\Publications\Models\PublicationFieldDefinition;
10use Hyde\Publications\Pages\PublicationPage;
11use Hyde\Publications\Publications;
12use Illuminate\Contracts\View\View;
13use Illuminate\Support\Collection;
14use Illuminate\View\Component;
15
16use function collect;
17use function count;
18use function view;
19
20class 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}