Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
100.00% |
56 / 56 |
|
100.00% |
34 / 34 |
CRAP | |
100.00% |
1 / 1 |
HydePage | |
100.00% |
56 / 56 |
|
100.00% |
34 / 34 |
39 | |
100.00% |
1 / 1 |
make | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
isDiscoverable | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
get | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
parse | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
files | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
all | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sourceDirectory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
outputDirectory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
fileExtension | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setSourceDirectory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setOutputDirectory | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
setFileExtension | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
sourcePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
outputPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
path | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
pathToIdentifier | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
baseRouteKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
compile | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
toArray | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
getSourcePath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getOutputPath | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRouteKey | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getRoute | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getLink | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getIdentifier | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getBladeView | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
title | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
metadata | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
showInNavigation | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
navigationMenuPriority | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
navigationMenuLabel | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
navigationMenuGroup | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getCanonicalUrl | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
4 | |||
constructMetadata | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 |
1 | <?php |
2 | |
3 | declare(strict_types=1); |
4 | |
5 | namespace Hyde\Pages\Concerns; |
6 | |
7 | use Hyde\Hyde; |
8 | use Hyde\Facades\Config; |
9 | use Hyde\Foundation\Facades; |
10 | use Hyde\Foundation\Facades\Files; |
11 | use Hyde\Foundation\Facades\Pages; |
12 | use Hyde\Foundation\Facades\Routes; |
13 | use Hyde\Foundation\Kernel\PageCollection; |
14 | use Hyde\Framework\Actions\SourceFileParser; |
15 | use Hyde\Framework\Concerns\InteractsWithFrontMatter; |
16 | use Hyde\Framework\Factories\Concerns\HasFactory; |
17 | use Hyde\Framework\Features\Metadata\PageMetadataBag; |
18 | use Hyde\Framework\Features\Navigation\NavigationData; |
19 | use Hyde\Markdown\Contracts\FrontMatter\PageSchema; |
20 | use Hyde\Markdown\Models\FrontMatter; |
21 | use Hyde\Support\Concerns\Serializable; |
22 | use Hyde\Support\Contracts\SerializableContract; |
23 | use Hyde\Support\Filesystem\SourceFile; |
24 | use Hyde\Support\Models\Route; |
25 | use Hyde\Support\Models\RouteKey; |
26 | use Illuminate\Support\Str; |
27 | |
28 | use function Hyde\unslash; |
29 | use function filled; |
30 | use function ltrim; |
31 | use function rtrim; |
32 | |
33 | /** |
34 | * The base class for all Hyde pages. |
35 | * |
36 | * To ensure compatibility with the Hyde Framework, all page models should extend this class. |
37 | * Markdown-based pages can extend the BaseMarkdownPage class to get relevant helpers. |
38 | * |
39 | * Unlike other frameworks, in general you don't instantiate pages yourself in Hyde, |
40 | * instead, the page models acts as blueprints defining information for Hyde to |
41 | * know how to parse a file, and what data around it should be generated. |
42 | * |
43 | * To create a parsed file instance, you'd typically just create a source file, |
44 | * and you can then access the parsed file from the HydeKernel's page index. |
45 | * The source files are usually parsed by the SourceFileParser action. |
46 | * |
47 | * In Blade views, you can always access the current page instance being rendered using the $page variable. |
48 | * |
49 | * @see \Hyde\Pages\Concerns\BaseMarkdownPage |
50 | */ |
51 | abstract class HydePage implements PageSchema, SerializableContract |
52 | { |
53 | use InteractsWithFrontMatter; |
54 | use Serializable; |
55 | use HasFactory; |
56 | |
57 | public static string $sourceDirectory; |
58 | public static string $outputDirectory; |
59 | public static string $fileExtension; |
60 | public static string $template; |
61 | |
62 | public readonly string $identifier; |
63 | public readonly string $routeKey; |
64 | public readonly string $title; |
65 | |
66 | public FrontMatter $matter; |
67 | public PageMetadataBag $metadata; |
68 | public NavigationData $navigation; |
69 | |
70 | /** |
71 | * Create a new page instance. Static alias for the constructor. |
72 | */ |
73 | public static function make(string $identifier = '', FrontMatter|array $matter = []): static |
74 | { |
75 | return new static($identifier, $matter); |
76 | } |
77 | |
78 | /** |
79 | * Construct a new page instance. |
80 | */ |
81 | public function __construct(string $identifier = '', FrontMatter|array $matter = []) |
82 | { |
83 | $this->identifier = $identifier; |
84 | $this->routeKey = RouteKey::fromPage(static::class, $identifier)->get(); |
85 | $this->matter = $matter instanceof FrontMatter ? $matter : new FrontMatter($matter); |
86 | |
87 | $this->constructFactoryData(); |
88 | $this->constructMetadata(); |
89 | } |
90 | |
91 | // Section: State |
92 | |
93 | /** |
94 | * Returns whether the page type is discoverable through auto-discovery. |
95 | */ |
96 | public static function isDiscoverable(): bool |
97 | { |
98 | return isset(static::$sourceDirectory, static::$outputDirectory, static::$fileExtension) && filled(static::$sourceDirectory); |
99 | } |
100 | |
101 | // Section: Query |
102 | |
103 | /** |
104 | * Get a page instance from the Kernel's page index by its identifier. |
105 | * |
106 | * @throws \Hyde\Framework\Exceptions\FileNotFoundException If the page does not exist. |
107 | */ |
108 | public static function get(string $identifier): static |
109 | { |
110 | return Pages::getPage(static::sourcePath($identifier)); |
111 | } |
112 | |
113 | /** |
114 | * Parse a source file into a new page model instance. |
115 | * |
116 | * @param string $identifier The identifier of the page to parse. |
117 | * @return static New page model instance for the parsed source file. |
118 | * |
119 | * @throws \Hyde\Framework\Exceptions\FileNotFoundException If the file does not exist. |
120 | */ |
121 | public static function parse(string $identifier): static |
122 | { |
123 | return (new SourceFileParser(static::class, $identifier))->get(); |
124 | } |
125 | |
126 | /** |
127 | * Get an array of all the source file identifiers for the model. |
128 | * |
129 | * Note that the values do not include the source directory or file extension. |
130 | * |
131 | * @return array<string> |
132 | */ |
133 | public static function files(): array |
134 | { |
135 | return Files::getFiles(static::class)->map(function (SourceFile $file): string { |
136 | return static::pathToIdentifier($file->getPath()); |
137 | })->values()->toArray(); |
138 | } |
139 | |
140 | /** |
141 | * Get a collection of all pages, parsed into page models. |
142 | * |
143 | * @return \Hyde\Foundation\Kernel\PageCollection<static> |
144 | */ |
145 | public static function all(): PageCollection |
146 | { |
147 | return Facades\Pages::getPages(static::class); |
148 | } |
149 | |
150 | // Section: Filesystem |
151 | |
152 | /** |
153 | * Get the directory where source files are stored for the page type. |
154 | */ |
155 | public static function sourceDirectory(): string |
156 | { |
157 | return static::$sourceDirectory ?? Hyde::getSourceRoot(); |
158 | } |
159 | |
160 | /** |
161 | * Get the output subdirectory to store compiled HTML files for the page type. |
162 | */ |
163 | public static function outputDirectory(): string |
164 | { |
165 | return static::$outputDirectory ?? ''; |
166 | } |
167 | |
168 | /** |
169 | * Get the file extension of the source files for the page type. |
170 | */ |
171 | public static function fileExtension(): string |
172 | { |
173 | return static::$fileExtension ?? ''; |
174 | } |
175 | |
176 | /** |
177 | * Set the output directory for the page type. |
178 | */ |
179 | public static function setSourceDirectory(string $sourceDirectory): void |
180 | { |
181 | static::$sourceDirectory = unslash($sourceDirectory); |
182 | } |
183 | |
184 | /** |
185 | * Set the source directory for the page type. |
186 | */ |
187 | public static function setOutputDirectory(string $outputDirectory): void |
188 | { |
189 | static::$outputDirectory = unslash($outputDirectory); |
190 | } |
191 | |
192 | /** |
193 | * Set the file extension for the page type. |
194 | */ |
195 | public static function setFileExtension(string $fileExtension): void |
196 | { |
197 | static::$fileExtension = rtrim('.'.ltrim($fileExtension, '.'), '.'); |
198 | } |
199 | |
200 | /** |
201 | * Qualify a page identifier into file path to the source file, relative to the project root. |
202 | */ |
203 | public static function sourcePath(string $identifier): string |
204 | { |
205 | return unslash(static::sourceDirectory().'/'.unslash($identifier).static::fileExtension()); |
206 | } |
207 | |
208 | /** |
209 | * Qualify a page identifier into a target output file path, relative to the _site output directory. |
210 | */ |
211 | public static function outputPath(string $identifier): string |
212 | { |
213 | return RouteKey::fromPage(static::class, $identifier).'.html'; |
214 | } |
215 | |
216 | /** |
217 | * Get an absolute file path to the page's source directory, or a file within it. |
218 | */ |
219 | public static function path(string $path = ''): string |
220 | { |
221 | return Hyde::path(unslash(static::sourceDirectory().'/'.unslash($path))); |
222 | } |
223 | |
224 | /** |
225 | * Format a filename to an identifier for a given model. Unlike the basename function, any nested paths |
226 | * within the source directory are retained in order to satisfy the page identifier definition. |
227 | * |
228 | * @param string $path Example: index.blade.php |
229 | * @return string Example: index |
230 | */ |
231 | public static function pathToIdentifier(string $path): string |
232 | { |
233 | return unslash(Str::between(Hyde::pathToRelative($path), |
234 | static::sourceDirectory().'/', |
235 | static::fileExtension()) |
236 | ); |
237 | } |
238 | |
239 | /** |
240 | * Get the route key base for the page model. |
241 | * |
242 | * This is the same value as the output directory. |
243 | */ |
244 | public static function baseRouteKey(): string |
245 | { |
246 | return static::outputDirectory(); |
247 | } |
248 | |
249 | /** |
250 | * Compile the page into static HTML. |
251 | * |
252 | * @return string The compiled HTML for the page. |
253 | */ |
254 | abstract public function compile(): string; |
255 | |
256 | /** |
257 | * Get the instance as an array. |
258 | */ |
259 | public function toArray(): array |
260 | { |
261 | return [ |
262 | 'class' => static::class, |
263 | 'identifier' => $this->identifier, |
264 | 'routeKey' => $this->routeKey, |
265 | 'matter' => $this->matter, |
266 | 'metadata' => $this->metadata, |
267 | 'navigation' => $this->navigation, |
268 | 'title' => $this->title, |
269 | ]; |
270 | } |
271 | |
272 | /** |
273 | * Get the path to the instance source file, relative to the project root. |
274 | */ |
275 | public function getSourcePath(): string |
276 | { |
277 | return unslash(static::sourcePath($this->identifier)); |
278 | } |
279 | |
280 | /** |
281 | * Get the path where the compiled page will be saved. |
282 | * |
283 | * @return string Path relative to the site output directory. |
284 | */ |
285 | public function getOutputPath(): string |
286 | { |
287 | return unslash(static::outputPath($this->identifier)); |
288 | } |
289 | |
290 | // Section: Routing |
291 | |
292 | /** |
293 | * Get the route key for the page. |
294 | * |
295 | * The route key is the page URL path, relative to the site root, but without any file extensions. |
296 | * For example, if the page will be saved to `_site/docs/index.html`, the key is `docs/index`. |
297 | * |
298 | * Route keys are used to identify page routes, similar to how named routes work in Laravel, |
299 | * only that here the name is not just arbitrary, but also defines the output location, |
300 | * as the route key is used to determine the output path which is `$routeKey.html`. |
301 | */ |
302 | public function getRouteKey(): string |
303 | { |
304 | return $this->routeKey; |
305 | } |
306 | |
307 | /** |
308 | * Get the route object for the page. |
309 | */ |
310 | public function getRoute(): Route |
311 | { |
312 | return Routes::get($this->getRouteKey()) ?? new Route($this); |
313 | } |
314 | |
315 | /** |
316 | * Format the page instance to a URL path, with support for pretty URLs if enabled. |
317 | * |
318 | * Note that the link is always relative to site root, and does not contain `../` segments. |
319 | */ |
320 | public function getLink(): string |
321 | { |
322 | return Hyde::formatLink($this->getOutputPath()); |
323 | } |
324 | |
325 | // Section: Getters |
326 | |
327 | /** |
328 | * Get the page model's identifier property. |
329 | * |
330 | * The identifier is the part between the source directory and the file extension. |
331 | * It may also be known as a 'slug', or previously 'basename', but it retains |
332 | * the nested path structure if the page is stored in a subdirectory. |
333 | * |
334 | * For example, the identifier of a source file stored as '_pages/about/contact.md' |
335 | * would be 'about/contact', and 'pages/about.md' would simply be 'about'. |
336 | */ |
337 | public function getIdentifier(): string |
338 | { |
339 | return $this->identifier; |
340 | } |
341 | |
342 | /** |
343 | * Get the Blade template/view key for the page. |
344 | */ |
345 | public function getBladeView(): string |
346 | { |
347 | return static::$template; |
348 | } |
349 | |
350 | // Section: Accessors |
351 | |
352 | /** |
353 | * Get the page title to display in HTML tags like `<title>` and `<meta>` tags. |
354 | */ |
355 | public function title(): string |
356 | { |
357 | return Config::getString('hyde.name', 'HydePHP').' - '.$this->title; |
358 | } |
359 | |
360 | /** |
361 | * Get the generated metadata for the page. |
362 | */ |
363 | public function metadata(): PageMetadataBag |
364 | { |
365 | return $this->metadata; |
366 | } |
367 | |
368 | /** |
369 | * Can the page be shown in the navigation menu? |
370 | */ |
371 | public function showInNavigation(): bool |
372 | { |
373 | return ! $this->navigation->hidden; |
374 | } |
375 | |
376 | /** |
377 | * Get the priority of the page in the navigation menu. |
378 | */ |
379 | public function navigationMenuPriority(): int |
380 | { |
381 | return $this->navigation->priority; |
382 | } |
383 | |
384 | /** |
385 | * Get the label of the page in the navigation menu. |
386 | */ |
387 | public function navigationMenuLabel(): string |
388 | { |
389 | return $this->navigation->label; |
390 | } |
391 | |
392 | /** |
393 | * Get the group of the page in the navigation menu, if any. |
394 | */ |
395 | public function navigationMenuGroup(): ?string |
396 | { |
397 | return $this->navigation->group; |
398 | } |
399 | |
400 | /** |
401 | * Get the canonical URL for the page to use in the `<link rel="canonical">` tag. |
402 | * |
403 | * It can be explicitly set in the front matter using the `canonicalUrl` key, |
404 | * otherwise it will be generated based on the site URL and the output path, |
405 | * unless there is no configured base URL, leading to this returning null. |
406 | */ |
407 | public function getCanonicalUrl(): ?string |
408 | { |
409 | /** @var ?string $value */ |
410 | $value = $this->matter('canonicalUrl'); |
411 | |
412 | if (! empty($value)) { |
413 | return $value; |
414 | } |
415 | |
416 | if (Hyde::hasSiteUrl() && ! empty($this->identifier)) { |
417 | return Hyde::url($this->getOutputPath()); |
418 | } |
419 | |
420 | return null; |
421 | } |
422 | |
423 | protected function constructMetadata(): void |
424 | { |
425 | $this->metadata = new PageMetadataBag($this); |
426 | } |
427 | } |