IndexManager::getTagIndex()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 2
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

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