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 {
|
|
|
|
|
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 {
|
|
|
|
|
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 {
|
|
|
|
|
86
|
|
|
return isset($this->slugIndex[$key]);
|
87
|
|
|
}
|
88
|
|
|
|
89
|
|
|
/**
|
90
|
|
|
* Return the posts index.
|
91
|
|
|
* @return array<string, Element>
|
92
|
|
|
*/
|
93
|
|
|
function getPostsIndex(): array {
|
|
|
|
|
94
|
|
|
return $this->postIndex;
|
95
|
|
|
}
|
96
|
|
|
|
97
|
|
|
/**
|
98
|
|
|
* Return the category index.
|
99
|
|
|
* @return array<string, Element>
|
100
|
|
|
*/
|
101
|
|
|
function getCategoriesIndex(): array {
|
|
|
|
|
102
|
|
|
return $this->catIndex;
|
103
|
|
|
}
|
104
|
|
|
|
105
|
|
|
/**
|
106
|
|
|
* Return the tag index.
|
107
|
|
|
* @return array<string, Element>
|
108
|
|
|
*/
|
109
|
|
|
function getTagIndex(): array {
|
|
|
|
|
110
|
|
|
return $this->tagIndex;
|
111
|
|
|
}
|
112
|
|
|
|
113
|
|
|
/**
|
114
|
|
|
* Return the menus index.
|
115
|
|
|
* @return array<mixed>
|
116
|
|
|
*/
|
117
|
|
|
function getMenusIndex(): array {
|
|
|
|
|
118
|
|
|
return $this->menuIndex;
|
119
|
|
|
}
|
120
|
|
|
|
121
|
|
|
/**
|
122
|
|
|
* Return the tag index.
|
123
|
|
|
* @return array<string, Element>
|
124
|
|
|
*/
|
125
|
|
|
function getPageIndex(): array {
|
|
|
|
|
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);
|
|
|
|
|
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);
|
|
|
|
|
211
|
|
|
$index[(string)$element->key] = $element;
|
|
|
|
|
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
|
|
|
|
|
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
|
|
|
|
Adding explicit visibility (
private
,protected
, orpublic
) is generally recommend to communicate to other developers how, and from where this method is intended to be used.