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
|
|
|
|