Passed
Push — main ( ffd35e...44834b )
by Marc
03:18
created

IndexManager::getPostsForCategoryIndex()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 0
dl 0
loc 2
rs 10
1
<?php declare(strict_types=1);
2
namespace html_go\indexing;
3
4
use InvalidArgumentException;
5
use html_go\exceptions\InternalException;
6
7
final class IndexManager extends AbstractIndexer
8
{
9
    /** @var array<string, \stdClass> $catIndex */
10
    private array $catIndex;
11
12
    /** @var array<string, \stdClass> $pageIndex */
13
    private array $pageIndex;
14
15
    /** @var array<string, \stdClass> $postIndex */
16
    private array $postIndex;
17
18
    /** @var array<mixed> $menuIndex */
19
    private array $menuIndex;
20
21
    /**
22
     * A tag is NOT represented by any file on the physical filessytem.
23
     * @var array<string, \stdClass> $tagIndex
24
     */
25
    private array $tagIndex;
26
27
    /**
28
     * The slug index holds references to files on the physical filesystem.,
29
     * and is a combination of the catIndex, postIndex and pageIndex
30
     * @var array<string, \stdClass> $slugIndex
31
     */
32
    private array $slugIndex;
33
34
    /** @var array<string, \stdClass> $tag2postIndex */
35
    private array $tag2postIndex;
36
37
    /** @var array<string, array> $cat2postIndex */
38
    private array $cat2postIndex;
39
40
    /**
41
     * IndexManager constructor.
42
     * @param string $parentDir The parent directory for the content directory.
43
     * @throws \InvalidArgumentException If the parent directory is invalid.
44
     * @throws InternalException
45
     */
46
    public function __construct(string $parentDir) {
47
        parent::__construct($parentDir);
48
        $this->initialize();
49
    }
50
51
    /**
52
     * Rebuild all the indexes.
53
     */
54
    public function reindex(): void {
55
        $this->catIndex = $this->buildCategoryIndex();
56
        $pageMenuIndex = $this->buildPageAndMenuIndexes();
57
        $this->pageIndex = $pageMenuIndex[0];
58
        $this->menuIndex = $pageMenuIndex[1];
59
        $this->postIndex = $this->buildPostIndex();
60
        $compositeIndex = $this->buildCompositeIndexes();
61
        $this->tagIndex = $compositeIndex[0];
62
        $this->tag2postIndex = $compositeIndex[1];
63
        $this->cat2postIndex = $compositeIndex[2];
64
        $this->slugIndex = \array_merge($this->postIndex, $this->catIndex, $this->pageIndex, $this->tagIndex);
65
    }
66
67
    /**
68
     * Returns an object representing an element in the index.
69
     * @param string $key
70
     * @throws \InvalidArgumentException If the given $key does not exist in the index.
71
     * @return \stdClass
72
     */
73
    public function getElementFromSlugIndex(string $key): \stdClass {
74
        if (!isset($this->slugIndex[$key])) {
75
            throw new InvalidArgumentException("Key does not exist in the slugIndex! Use 'elementExists()' before calling this method.");
76
        }
77
        return $this->slugIndex[$key];
78
    }
79
80
    /**
81
     * Check if an key exists in the <b>slug index</b>.
82
     * @param string $key
83
     * @return bool <code>true</code> if exists, otherwise <code>false</code>
84
     */
85
    public function elementExists(string $key): bool {
86
        return isset($this->slugIndex[$key]);
87
    }
88
89
    /**
90
     * Return the posts index.
91
     * @return array<string, \stdClass>
92
     */
93
    public function getPostsIndex(): array {
94
        return $this->postIndex;
95
    }
96
97
    /**
98
     * Return the category index.
99
     * @return array<string, \stdClass>
100
     */
101
    public function getCategoriesIndex(): array {
102
        return $this->catIndex;
103
    }
104
105
    /**
106
     * Return the tag index.
107
     * @return array<string, \stdClass>
108
     */
109
    public function getTagIndex(): array {
110
        return $this->tagIndex;
111
    }
112
113
    /**
114
     * Return the menus index.
115
     * @return array<mixed>
116
     */
117
    public function getMenusIndex(): array {
118
        return $this->menuIndex;
119
    }
120
121
    /**
122
     * Return the tag index.
123
     * @return array<string, \stdClass>
124
     */
125
    public function getPageIndex(): array {
126
        return $this->pageIndex;
127
    }
128
129
    /**
130
     * Return the posts for category index.
131
     * @return array<string, array>
132
     */
133
    public function getPostsForCategoryIndex(): array {
134
        return $this->cat2postIndex;
135
    }
136
137
    private function initialize(): void {
138
        if ((\is_dir($this->parentDir.DS.'cache'.DS.'indexes')) === false) {
139
            $dir = $this->parentDir.DS.'cache'.DS.'indexes';
140
            if (\mkdir($dir, MODE, true) === false) {
141
                throw new InternalException("Unable to create cache/indexes directory [$dir]"); // @codeCoverageIgnore
142
            }
143
            $this->reindex();
144
        } else {
145
            $this->catIndex = $this->loadIndex($this->catInxFile);
146
            $this->pageIndex = $this->loadIndex($this->pageInxFile);
147
            $this->postIndex = $this->loadIndex($this->postInxFile);
148
            $this->tagIndex = $this->loadIndex($this->tagInxFile);
149
            $this->cat2postIndex = $this->loadIndex($this->cat2postInxFile);
150
            $this->tag2postIndex = $this->loadIndex($this->tag2postInxFile);
151
            $this->menuIndex = $this->loadIndex($this->menuInxFile);
152
            $this->slugIndex = \array_merge($this->postIndex, $this->catIndex, $this->pageIndex, $this->tagIndex);
153
        }
154
    }
155
156
    /**
157
     * @return array<string, \stdClass>
158
     */
159
    private function buildCategoryIndex(): array {
160
        $index = [];
161
        foreach ($this->parseDirectory($this->commonDir.DS.CATEGORY_SECTION.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
162
            $root = \substr($filepath, \strlen($this->commonDir) + 1);
163
            $key = \str_replace(DS, FWD_SLASH, \substr($root, 0, \strlen($root) - CONTENT_FILE_EXT_LEN));
164
            if (CATEGORY_SECTION.FWD_SLASH.'index' === $key) {
165
                continue; // 'index.json' file is the landing page
166
            }
167
            $index[$key] = $this->createElement($key, $filepath, CATEGORY_SECTION);
168
        }
169
        $this->writeIndex($this->catInxFile, $index);
170
        return $index;
171
    }
172
173
    /**
174
     * Builds two indexes: menu and post indexes.
175
     * @return array<mixed>
176
     */
177
    private function buildPageAndMenuIndexes(): array {
178
        $pageDir = $this->commonDir.DS.PAGE_SECTION;
179
        $len = \strlen($pageDir) + 1;
180
        $pages = $this->scanDirectory($pageDir);
181
        \sort($pages);
182
        $menuInx = [];
183
        $pageInx = [];
184
        foreach ($pages as $filepath) {
185
            $location = \substr($filepath, $len);
186
            $key = \str_replace(DS, FWD_SLASH, \substr($location, 0, (\strlen($location) - CONTENT_FILE_EXT_LEN)));
187
            if (\str_ends_with($key, '/index')) {
188
                $key = substr($key, 0, \strlen($key) - 6);
189
            } else {
190
                if ($key === 'index') {
191
                    $key = FWD_SLASH;
192
                }
193
            }
194
            $pageInx[$key] = $this->createElement($key, $filepath, PAGE_SECTION);
195
            $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
196
        }
197
198
        // Add Tag landing page
199
        $filepath = $this->commonDir.DS.TAG_SECTION.DS.'index'.CONTENT_FILE_EXT;
200
        $pageInx[TAG_INDEX_KEY] = $this->createElement(TAG_SECTION, $filepath, TAG_SECTION);
201
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus(TAG_SECTION, $filepath));
202
203
        // Add Category landing page
204
        $filepath = $this->commonDir.DS.CATEGORY_SECTION.DS.'index'.CONTENT_FILE_EXT;
205
        $pageInx[CAT_INDEX_KEY] = $this->createElement(CATEGORY_SECTION, $filepath, CATEGORY_SECTION);
206
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus(CATEGORY_SECTION, $filepath));
207
208
        // Add Blog (posts) landing page
209
        $filepath = $this->commonDir.DS.POST_SECTION.DS.'index'.CONTENT_FILE_EXT;
210
        $pageInx[POST_INDEX_KEY] = $this->createElementClass(POST_INDEX_KEY, $filepath, POST_SECTION);
211
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus(POST_INDEX_KEY, $filepath));
212
213
        $this->writeIndex($this->pageInxFile, $pageInx);
214
        $menuInx = $this->orderMenuEntries($menuInx);
215
        $this->writeIndex($this->menuInxFile, $menuInx);
216
        return [$pageInx, $menuInx];
217
    }
218
219
    /**
220
     * Builds the post index.
221
     * @return array<string, \stdClass>
222
     */
223
    private function buildPostIndex(): array {
224
        $index = [];
225
        foreach ($this->parseDirectory($this->userDataDir.DS.'*'.DS.POST_SECTION.DS.'*'.DS.'*'.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
226
            $key = \pathinfo($filepath, PATHINFO_FILENAME);
227
            $element = $this->createElement(/** @scrutinizer ignore-type */ $key, $filepath, POST_SECTION);
228
            $index[(string)$element->key] = $element;
229
        }
230
        $this->writeIndex($this->postInxFile, $index);
231
        return $index;
232
    }
233
234
    /**
235
     * Reads the given file and creates an array of menus in which this
236
     * resource is listed.
237
     * @return array<mixed>
238
     */
239
    private function buildMenus(string $key, string $filepath): array {
240
        if (empty($key)) {
241
            throw new \InvalidArgumentException("Key is empty for [$filepath]"); // @codeCoverageIgnore
242
        }
243
        if (($json = \file_get_contents($filepath)) === false) {
244
            throw new InternalException("file_get_contents() failed reading [$filepath]"); // @codeCoverageIgnore
245
        }
246
        $data = \json_decode($json, true);
247
        $menus = [];
248
        if (isset($data['menus'])) {
249
            foreach ($data['menus'] as $name => $defs) {
250
                $node = new \stdClass();
251
                $node->key = $key;
252
                foreach ($defs as $label => $value) {
253
                    $node->$label = $value;
254
                }
255
                $menus[$name][$node->key] = $node;
256
            }
257
        }
258
        return $menus;
259
    }
260
261
    /**
262
     * Does a <code>usort</code> on the <code>weight</code> property.
263
     * @param array<mixed> $index the unsorted array
264
     * @return array<mixed> the sorted array
265
     */
266
    private function orderMenuEntries(array $index): array {
267
        foreach ($index as $name => $defs) {
268
            \usort($defs, function($a, $b): int {
269
                if ($a->weight === $b->weight) {
270
                    return 0;
271
                }
272
                return  $a->weight > $b->weight ? 1 : -1;
273
            });
274
            $index[$name] = $defs;
275
        }
276
        return $index;
277
    }
278
279
    /**
280
     * Builds three indexes: 'category 2 posts', 'tag 2 posts' and tag index.
281
     * @throws InternalException
282
     * @return array<mixed>
283
     */
284
    private function buildCompositeIndexes(): array {
285
        $tagInx = [];
286
        $tag2PostsIndex = [];
287
        $cat2PostIndex = [];
288
        foreach ($this->postIndex as $post) {
289
            if (!isset($post->key, $post->tags, $post->category)) {
290
                throw new InternalException("Invalid format of index element: "./** @scrutinizer ignore-type */print_r($post, true)); // @codeCoverageIgnore
291
            }
292
            foreach ($post->tags as $tag) {
293
                $key = TAG_SECTION.FWD_SLASH.(string)$tag;
294
                $tagInx[$key] = $this->createElementClass($key, EMPTY_VALUE, TAG_SECTION);
295
                $tag2PostsIndex[$key][] = $post->key;
296
            }
297
            $cat2PostIndex[$post->category][] = $post->key;
298
        }
299
        $this->writeIndex($this->tagInxFile, $tagInx);
300
        $this->writeIndex($this->tag2postInxFile, $tag2PostsIndex);
301
        $this->writeIndex($this->cat2postInxFile, $cat2PostIndex);
302
        return [$tagInx, $tag2PostsIndex, $cat2PostIndex];
303
    }
304
305
    /**
306
     * Merge the given menu array into the master menu index returning the new
307
     * master menu index.
308
     * @param array<mixed> $initial
309
     * @param array<mixed> $toMerge The menu array to be merged.
310
     * @return array<mixed>
311
     */
312
    private function mergeToMenuIndex(array $initial, array $toMerge): array {
313
        foreach ($toMerge as $name => $def) {
314
            if (isset($initial[$name])) {
315
                $nodes = $initial[$name];
316
                $initial[$name] = \array_merge($nodes, $def);
317
            } else {
318
                $initial[$name] = $def;
319
            }
320
        }
321
        return $initial;
322
    }
323
}
324