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