Passed
Push — main ( d16942...b9546c )
by Marc
03:10
created

IndexManager::buildCompositeIndexes()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 19
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 15
nc 4
nop 0
dl 0
loc 19
rs 9.7666
c 0
b 0
f 0
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, \stdClass> $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
    private function initialize(): void {
130
        if ((\is_dir($this->parentDir.DS.'cache'.DS.'indexes')) === false) {
131
            $dir = $this->parentDir.DS.'cache'.DS.'indexes';
132
            if (\mkdir($dir, MODE, true) === false) {
133
                throw new InternalException("Unable to create cache/indexes directory [$dir]"); // @codeCoverageIgnore
134
            }
135
            $this->reindex();
136
        } else {
137
            $this->catIndex = $this->loadIndex($this->catInxFile);
138
            $this->pageIndex = $this->loadIndex($this->pageInxFile);
139
            $this->postIndex = $this->loadIndex($this->postInxFile);
140
            $this->tagIndex = $this->loadIndex($this->tagInxFile);
141
            $this->cat2postIndex = $this->loadIndex($this->cat2postInxFile);
142
            $this->tag2postIndex = $this->loadIndex($this->tag2postInxFile);
143
            $this->menuIndex = $this->loadIndex($this->menuInxFile);
144
            $this->slugIndex = \array_merge($this->postIndex, $this->catIndex, $this->pageIndex, $this->tagIndex);
145
        }
146
    }
147
148
    /**
149
     * @return array<string, \stdClass>
150
     */
151
    private function buildCategoryIndex(): array {
152
        $index = [];
153
        foreach ($this->parseDirectory($this->commonDir.DS.'category'.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
154
            $root = \substr($filepath, \strlen($this->commonDir) + 1);
155
            $key = \str_replace(DS, FWD_SLASH, \substr($root, 0, \strlen($root) - CONTENT_FILE_EXT_LEN));
156
            $index[$key] = $this->createElement($key, $filepath, ENUM_CATEGORY);
157
        }
158
        $this->writeIndex($this->catInxFile, $index);
159
        return $index;
160
    }
161
162
    /**
163
     * Builds two indexes: menu and post indexes.
164
     * @return array<mixed>
165
     */
166
    private function buildPageAndMenuIndexes(): array {
167
        $pageDir = $this->commonDir.DS.'pages';
168
        $len = \strlen($pageDir) + 1;
169
        $pages = $this->scanDirectory($pageDir);
170
        \sort($pages);
171
        $menuInx = [];
172
        $pageInx = [];
173
        foreach ($pages as $filepath) {
174
            $location = \substr($filepath, $len);
175
            $key = \str_replace(DS, FWD_SLASH, \substr($location, 0, (\strlen($location) - CONTENT_FILE_EXT_LEN)));
176
            if (\str_ends_with($key, '/index')) {
177
                $key = substr($key, 0, \strlen($key) - 6);
178
            } else {
179
                if ($key === 'index') {
180
                    $key = FWD_SLASH;
181
                }
182
            }
183
            $pageInx[$key] = $this->createElement($key, $filepath, ENUM_PAGE);
184
            $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
185
        }
186
187
        // Add Tag landing page
188
        $filepath = $this->commonDir.DS.'landing'.DS.'tags'.DS.'index'.CONTENT_FILE_EXT;
189
        $key = TAG_INDEX_KEY;
190
        $pageInx[$key] = $this->createElement($key, $filepath, ENUM_TAG);
191
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
192
193
        // Add Category landing page
194
        $filepath = $this->commonDir.DS.'landing'.DS.'category'.DS.'index'.CONTENT_FILE_EXT;
195
        $key = CAT_INDEX_KEY;
196
        $pageInx[$key] = $this->createElement($key, $filepath, ENUM_CATEGORY);
197
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
198
199
        // Add Blog (posts) landing page
200
        $filepath = $this->commonDir.DS.'landing'.DS.'blog'.DS.'index'.CONTENT_FILE_EXT;
201
        $key = BLOG_INDEX_KEY;
202
        $pageInx[$key] = $this->createElementClass($key, $filepath, ENUM_POST);
203
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
204
205
        $this->writeIndex($this->pageInxFile, $pageInx);
206
        $menuInx = $this->orderMenuEntries($menuInx);
207
        $this->writeIndex($this->menuInxFile, $menuInx);
208
        return [$pageInx, $menuInx];
209
    }
210
211
    /**
212
     * Builds the post index.
213
     * @return array<string, \stdClass>
214
     */
215
    private function buildPostIndex(): array {
216
        $index = [];
217
        foreach ($this->parseDirectory($this->userDataDir.DS.'*'.DS.'posts'.DS.'*'.DS.'*'.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
218
            $key = \pathinfo($filepath, PATHINFO_FILENAME);
219
            $element = $this->createElement(/** @scrutinizer ignore-type */ $key, $filepath, ENUM_POST);
220
            $index[(string)$element->key] = $element;
221
        }
222
        $this->writeIndex($this->postInxFile, $index);
223
        return $index;
224
    }
225
226
    /**
227
     * Reads the given file and creates an array of menus in which this
228
     * resource is listed.
229
     * @return array<mixed>
230
     */
231
    private function buildMenus(string $key, string $filepath): array {
232
        if (empty($key)) {
233
            throw new \InvalidArgumentException("Key is empty for [$filepath]"); // @codeCoverageIgnore
234
        }
235
        if (($json = \file_get_contents($filepath)) === false) {
236
            throw new InternalException("file_get_contents() failed reading [$filepath]"); // @codeCoverageIgnore
237
        }
238
        $data = \json_decode($json, true);
239
        $menus = [];
240
        if (isset($data['menus'])) {
241
            foreach ($data['menus'] as $name => $defs) {
242
                $node = new \stdClass();
243
                $node->key = $key;
244
                foreach ($defs as $label => $value) {
245
                    $node->$label = $value;
246
                }
247
                $menus[$name][] = $node;
248
            }
249
        }
250
        return $menus;
251
    }
252
253
    /**
254
     * Does a <code>usort</code> on the <code>weight</code> property.
255
     * @param array<mixed> $index the unsorted array
256
     * @return array<mixed> the sorted array
257
     */
258
    private function orderMenuEntries(array $index): array {
259
        foreach ($index as $name => $defs) {
260
            \usort($defs, function($a, $b): int {
261
                if ($a->weight === $b->weight) {
262
                    return 0;
263
                }
264
                return  $a->weight > $b->weight ? 1 : -1;
265
            });
266
            $index[$name] = $defs;
267
        }
268
        return $index;
269
    }
270
271
    /**
272
     * Builds three indexes: 'category 2 posts', 'tag 2 posts' and tag index.
273
     * @throws InternalException
274
     * @return array<mixed>
275
     */
276
    private function buildCompositeIndexes(): array {
277
        $tagInx = [];
278
        $tag2PostsIndex = [];
279
        $cat2PostIndex = [];
280
        foreach ($this->postIndex as $post) {
281
            if (!isset($post->key, $post->tags, $post->category)) {
282
                throw new InternalException("Invalid format of index element: "./** @scrutinizer ignore-type */print_r($post, true)); // @codeCoverageIgnore
283
            }
284
            foreach ($post->tags as $tag) {
285
                $key = 'tag'.FWD_SLASH.(string)$tag;
286
                $tagInx[$key] = $this->createElementClass($key, EMPTY_VALUE, ENUM_TAG);
287
                $tag2PostsIndex[$key][] = $post->key;
288
            }
289
            $cat2PostIndex[$post->category] = $post->key;
290
        }
291
        $this->writeIndex($this->tagInxFile, $tagInx);
292
        $this->writeIndex($this->tag2postInxFile, $tag2PostsIndex);
293
        $this->writeIndex($this->cat2postInxFile, $cat2PostIndex);
294
        return [$tagInx, $tag2PostsIndex, $cat2PostIndex];
295
    }
296
297
    /**
298
     * Merge the given menu array into the master menu index returning the new
299
     * master menu index.
300
     * @param array<mixed> $initial
301
     * @param array<mixed> $toMerge The menu array to be merged.
302
     * @return array<mixed>
303
     */
304
    private function mergeToMenuIndex(array $initial, array $toMerge): array {
305
        foreach ($toMerge as $name => $def) {
306
            if (isset($initial[$name])) {
307
                $nodes = $initial[$name];
308
                $initial[$name] = \array_merge($nodes, $def);
309
            } else {
310
                $initial[$name] = $def;
311
            }
312
        }
313
        return $initial;
314
    }
315
}
316