Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
34 / 34
CRAP
100.00% covered (success)
100.00%
1 / 1
HydePage
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
34 / 34
39
100.00% covered (success)
100.00%
1 / 1
 make
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 isDiscoverable
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 files
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sourceDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outputDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fileExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setSourceDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setOutputDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setFileExtension
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 sourcePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 outputPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 path
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 pathToIdentifier
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 baseRouteKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 compile
n/a
0 / 0
n/a
0 / 0
0
 toArray
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 getSourcePath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutputPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRouteKey
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRoute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getIdentifier
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBladeView
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 title
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 metadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 showInNavigation
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 navigationMenuPriority
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 navigationMenuLabel
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 navigationMenuGroup
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCanonicalUrl
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 constructMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Pages\Concerns;
6
7use Hyde\Hyde;
8use Hyde\Facades\Config;
9use Hyde\Foundation\Facades;
10use Hyde\Foundation\Facades\Files;
11use Hyde\Foundation\Facades\Pages;
12use Hyde\Foundation\Facades\Routes;
13use Hyde\Foundation\Kernel\PageCollection;
14use Hyde\Framework\Actions\SourceFileParser;
15use Hyde\Framework\Concerns\InteractsWithFrontMatter;
16use Hyde\Framework\Factories\Concerns\HasFactory;
17use Hyde\Framework\Features\Metadata\PageMetadataBag;
18use Hyde\Framework\Features\Navigation\NavigationData;
19use Hyde\Markdown\Contracts\FrontMatter\PageSchema;
20use Hyde\Markdown\Models\FrontMatter;
21use Hyde\Support\Concerns\Serializable;
22use Hyde\Support\Contracts\SerializableContract;
23use Hyde\Support\Filesystem\SourceFile;
24use Hyde\Support\Models\Route;
25use Hyde\Support\Models\RouteKey;
26use Illuminate\Support\Str;
27
28use function Hyde\unslash;
29use function filled;
30use function ltrim;
31use 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 */
51abstract 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}