Passed
Push — main ( 23d81d...4ea6b8 )
by Marc
03:52
created

IndexManager::mergeToMenuIndex()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 10
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 7
nc 3
nop 2
dl 0
loc 10
rs 10
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, Element> $catIndex */
10
    private array $catIndex;
11
12
    /** @var array<string, Element> $pageIndex */
13
    private array $pageIndex;
14
15
    /** @var array<string, Element> $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, Element> $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, Element> $slugIndex
31
     */
32
    private array $slugIndex;
33
34
    /** @var array<string, Element> $tag2postIndex */
35
    private array $tag2postIndex;
36
37
    /** @var array<string, Element> $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
    function __construct(string $parentDir) {
47
        parent::__construct($parentDir);
48
        $this->initialize();
49
    }
50
51
    /**
52
     * Rebuild all the indexes.
53
     */
54
    function reindex(): void {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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 Element
72
     */
73
    function getElementFromSlugIndex(string $key): Element {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
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
    function elementExists(string $key): bool {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
86
        return isset($this->slugIndex[$key]);
87
    }
88
89
    /**
90
     * Return the posts index.
91
     * @return array<string, Element>
92
     */
93
    function getPostsIndex(): array {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
94
        return $this->postIndex;
95
    }
96
97
    /**
98
     * Return the category index.
99
     * @return array<string, Element>
100
     */
101
    function getCategoriesIndex(): array {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
102
        return $this->catIndex;
103
    }
104
105
    /**
106
     * Return the tag index.
107
     * @return array<string, Element>
108
     */
109
    function getTagIndex(): array {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
110
        return $this->tagIndex;
111
    }
112
113
    /**
114
     * Return the menus index.
115
     * @return array<mixed>
116
     */
117
    function getMenusIndex(): array {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
118
        return $this->menuIndex;
119
    }
120
121
    /**
122
     * Return the tag index.
123
     * @return array<string, Element>
124
     */
125
    function getPageIndex(): array {
0 ignored issues
show
Best Practice introduced by
It is generally recommended to explicitly declare the visibility for methods.

Adding explicit visibility (private, protected, or public) is generally recommend to communicate to other developers how, and from where this method is intended to be used.

Loading history...
126
        return $this->pageIndex;
127
    }
128
129
    private function initialize(): void {
130
        $this->catIndex = $this->loadIndex($this->catInxFile);
131
        $this->pageIndex = $this->loadIndex($this->pageInxFile);
132
        $this->postIndex = $this->loadIndex($this->postInxFile);
133
        $this->tagIndex = $this->loadIndex($this->tagInxFile);
134
        $this->cat2postIndex = $this->loadIndex($this->cat2postInxFile);
135
        $this->tag2postIndex = $this->loadIndex($this->tag2postInxFile);
136
        $this->menuIndex = $this->loadIndex($this->menuInxFile);
137
        $this->slugIndex = \array_merge($this->postIndex, $this->catIndex, $this->pageIndex, $this->tagIndex);
138
    }
139
140
    /**
141
     * @return array<string, Element>
142
     */
143
    private function buildCategoryIndex(): array {
144
        $index = [];
145
        foreach ($this->parseDirectory($this->commonDir.DS.'category'.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
146
            $root = \substr($filepath, \strlen($this->commonDir) + 1);
147
            $key = \str_replace(DS, FWD_SLASH, \substr($root, 0, \strlen($root) - CONTENT_FILE_EXT_LEN));
148
            $index[$key] = $this->createElement($key, $filepath, ENUM_CATEGORY);
149
        }
150
        $this->writeIndex($this->catInxFile, $index);
151
        return $index;
152
    }
153
154
    /**
155
     * Builds two indexes: menu and post indexes.
156
     * @return array<mixed>
157
     */
158
    private function buildPageAndMenuIndexes(): array {
159
        $pageDir = $this->commonDir.DS.'pages';
160
        $len = \strlen($pageDir) + 1;
161
        $pages = $this->scanDirectory($pageDir);
162
        \sort($pages);
163
        $menuInx = [];
164
        foreach ($pages as $filepath) {
165
            $location = \substr($filepath, $len);
166
            $key = \str_replace(DS, FWD_SLASH, \substr($location, 0, (\strlen($location) - CONTENT_FILE_EXT_LEN)));
167
            if (\str_ends_with($key, '/index')) {
168
                $key = substr($key, 0, \strlen($key) - 6);
169
            } else {
170
                if ($key === 'index') {
171
                    $key = FWD_SLASH;
172
                }
173
            }
174
            $pageInx[$key] = $this->createElement($key, $filepath, ENUM_PAGE);
175
            $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
176
        }
177
178
        // Add Tag landing page
179
        $filepath = $this->commonDir.DS.'landing'.DS.'tags'.DS.'index'.CONTENT_FILE_EXT;
180
        $key = TAG_INDEX_KEY;
181
        $pageInx[$key] = $this->createElement($key, $filepath, ENUM_TAG);
182
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
183
184
        // Add Category landing page
185
        $filepath = $this->commonDir.DS.'landing'.DS.'category'.DS.'index'.CONTENT_FILE_EXT;
186
        $key = CAT_INDEX_KEY;
187
        $pageInx[$key] = $this->createElement($key, $filepath, ENUM_CATEGORY);
188
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
189
190
        // Add Blog (posts) landing page
191
        $filepath = $this->commonDir.DS.'landing'.DS.'blog'.DS.'index'.CONTENT_FILE_EXT;
192
        $key = BLOG_INDEX_KEY;
193
        $pageInx[$key] = $this->createElementClass($key, $filepath, ENUM_POST);
194
        $menuInx = $this->mergeToMenuIndex($menuInx, $this->buildMenus($key, $filepath));
195
196
        $this->writeIndex($this->pageInxFile, $pageInx);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $pageInx seems to be defined by a foreach iteration on line 164. Are you sure the iterator is never empty, otherwise this variable is not defined?
Loading history...
197
        $menuInx = $this->orderMenuEntries($menuInx);
198
        $this->writeIndex($this->menuInxFile, $menuInx);
199
        return [$pageInx, $menuInx];
200
    }
201
202
    /**
203
     * Builds the post index.
204
     * @return array<string, Element>
205
     */
206
    private function buildPostIndex(): array {
207
        $index = [];
208
        foreach ($this->parseDirectory($this->userDataDir.DS.'*'.DS.'posts'.DS.'*'.DS.'*'.DS.'*'.CONTENT_FILE_EXT) as $filepath) {
209
            $key = \pathinfo($filepath, PATHINFO_FILENAME);
210
            $element = $this->createElement($key, $filepath, ENUM_POST);
0 ignored issues
show
Bug introduced by
It seems like $key can also be of type array; however, parameter $key of html_go\indexing\IndexManager::createElement() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

210
            $element = $this->createElement(/** @scrutinizer ignore-type */ $key, $filepath, ENUM_POST);
Loading history...
211
            $index[(string)$element->key] = $element;
0 ignored issues
show
Bug introduced by
The property key does not seem to exist on html_go\indexing\Element.
Loading history...
212
        }
213
        $this->writeIndex($this->postInxFile, $index);
214
        return $index;
215
    }
216
217
    /**
218
     * Reads the given file and creates an array of menus in which this
219
     * resource is listed.
220
     * @return array<mixed>
221
     */
222
    private function buildMenus(string $key, string $filepath): array {
223
        if (empty($key)) {
224
            throw new \InvalidArgumentException("Key is empty for [$filepath]"); // @codeCoverageIgnore
225
        }
226
        if (($json = \file_get_contents($filepath)) === false) {
227
            throw new InternalException("file_get_contents() failed reading [$filepath]"); // @codeCoverageIgnore
228
        }
229
        $data = \json_decode($json, true);
230
        $menus = [];
231
        if (isset($data['menus'])) {
232
            foreach ($data['menus'] as $name => $defs) {
233
                $node = new \stdClass();
234
                $node->key = $key;
235
                foreach ($defs as $label => $value) {
236
                    $node->$label = $value;
237
                }
238
                $menus[$name][] = $node;
239
            }
240
        }
241
        return $menus;
242
    }
243
244
    /**
245
     * Does a <code>usort</code> on the <code>weight</code> property.
246
     * @param array<mixed> $index the unsorted array
247
     * @return array<mixed> the sorted array
248
     */
249
    private function orderMenuEntries(array $index): array {
250
        foreach ($index as $name => $defs) {
251
            \usort($defs, function($a, $b): int {
252
                if ($a->weight === $b->weight) {
253
                    return 0;
254
                }
255
                return  $a->weight > $b->weight ? 1 : -1;
256
            });
257
            $index[$name] = $defs;
258
        }
259
        return $index;
260
    }
261
262
    /**
263
     * Builds three indexes: 'category 2 posts', 'tag 2 posts' and tag index.
264
     * @throws InternalException
265
     * @return array<mixed>
266
     */
267
    private function buildCompositeIndexes(): array {
268
        $tagInx = [];
269
        $tag2PostsIndex = [];
270
        $cat2PostIndex = [];
271
        foreach ($this->postIndex as $post) {
272
            if (!isset($post->key, $post->tags, $post->category)) {
273
                throw new InternalException("Invalid format of index element: ".print_r($post, true)); // @codeCoverageIgnore
0 ignored issues
show
Bug introduced by
Are you sure print_r($post, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

273
                throw new InternalException("Invalid format of index element: "./** @scrutinizer ignore-type */ print_r($post, true)); // @codeCoverageIgnore
Loading history...
274
            }
275
            foreach ($post->tags as $tag) {
276
                $key = 'tag'.FWD_SLASH.(string)$tag;
277
                $tagInx[$key] = $this->createElementClass($key, EMPTY_VALUE, ENUM_TAG);
278
                $tag2PostsIndex[$key][] = $post->key;
279
            }
280
            $cat2PostIndex[$post->category] = $post->key;
281
        }
282
        $this->writeIndex($this->tagInxFile, $tagInx);
283
        $this->writeIndex($this->tag2postInxFile, $tag2PostsIndex);
284
        $this->writeIndex($this->cat2postInxFile, $cat2PostIndex);
285
        return [$tagInx, $tag2PostsIndex, $cat2PostIndex];
286
    }
287
288
    /**
289
     * Merge the given menu array into the master menu index returning the new
290
     * master menu index.
291
     * @param array<mixed> $initial
292
     * @param array<mixed> $toMerge The menu array to be merged.
293
     * @return array<mixed>
294
     */
295
    private function mergeToMenuIndex(array $initial, array $toMerge): array {
296
        foreach ($toMerge as $name => $def) {
297
            if (isset($initial[$name])) {
298
                $nodes = $initial[$name];
299
                $initial[$name] = \array_merge($nodes, $def);
300
            } else {
301
                $initial[$name] = $def;
302
            }
303
        }
304
        return $initial;
305
    }
306
307
    /**
308
     * Create an Element object.
309
     * @param string $key
310
     * @param string $filepath
311
     * @param string $section
312
     * @throws InternalException
313
     * @throws InvalidArgumentException
314
     * @return Element
315
     */
316
    private function createElement(string $key, string $filepath, string $section): Element {
317
        if (empty($key)) {
318
            throw new InternalException("Key is empty for [$filepath]"); // @codeCoverageIgnore
319
        }
320
        switch ($section) {
321
            case ENUM_CATEGORY:
322
            case ENUM_TAG:
323
            case ENUM_PAGE:
324
                return $this->createElementClass($key, $filepath, $section);
325
            case ENUM_POST:
326
                if (\strlen($key) < 17) {
327
                    throw new InvalidArgumentException("Post content filename is too short [$key]");
328
                }
329
                $pathinfo = \pathinfo($filepath);
330
                $dateString = \substr($key, 0, 14);
331
                $start = 15;
332
                if (($end = \strpos($key, '_', $start)) === false) {
333
                    throw new InvalidArgumentException("Post content filename syntax error [$key]");
334
                }
335
                $tagList = \substr($key, $start, $end - $start);
336
                $title = \substr($key, $end + 1);
337
                $year = \substr($dateString, 0, 4);
338
                $month = \substr($dateString, 4, 2);
339
                $key = $year.FWD_SLASH.$month.FWD_SLASH.$title;
340
                $parts = \explode(DS, $pathinfo['dirname']);
341
                $cnt = \count($parts);
342
                return $this->createElementClass($key, $filepath, ENUM_POST, type: $parts[$cnt - 1], category: $parts[$cnt - 2], username: $parts[$cnt - 4], date: $dateString, tags: $tagList);
343
            default:
344
                throw new InternalException("Unknown section [$section]"); // @codeCoverageIgnore
345
        }
346
    }
347
}
348