Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
23 / 23
CRAP
100.00% covered (success)
100.00%
1 / 1
PublicationType
100.00% covered (success)
100.00%
60 / 60
100.00% covered (success)
100.00%
23 / 23
28
100.00% covered (success)
100.00%
1 / 1
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 fromFile
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
2
 __construct
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 toArray
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
2
 toJson
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
 getSchemaFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDirectory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setMetadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFields
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFieldDefinition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCanonicalFieldDefinition
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getPublications
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPaginator
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 getListPage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 usesPagination
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 save
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 parseSchemaFile
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRelativeDirectoryEntry
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parseFieldData
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 withoutNullValues
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 validateSchemaFile
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Hyde\Publications\Models;
6
7use Exception;
8use Hyde\Framework\Concerns\InteractsWithDirectories;
9use Hyde\Hyde;
10use Hyde\Publications\Actions\PublicationSchemaValidator;
11use Hyde\Publications\Pages\PublicationListPage;
12use Hyde\Publications\Publications;
13use Hyde\Support\Concerns\Serializable;
14use Hyde\Support\Contracts\SerializableContract;
15use Hyde\Support\Paginator;
16use Illuminate\Support\Collection;
17use Illuminate\Support\Str;
18use RuntimeException;
19
20use function array_filter;
21use function array_merge;
22use function dirname;
23use function file_get_contents;
24use function file_put_contents;
25use function is_null;
26use function json_decode;
27use function json_encode;
28
29/**
30 * @see \Hyde\Publications\Testing\Feature\PublicationTypeTest
31 */
32class PublicationType implements SerializableContract
33{
34    use Serializable;
35    use InteractsWithDirectories;
36
37    /** The "pretty" name of the publication type */
38    public string $name;
39
40    /**
41     * The field name that is used as the canonical (or identifying) field of publications.
42     *
43     * It's used primarily for generating filenames, and the publications must thus be unique by this field.
44     */
45    public string $canonicalField = '__createdAt';
46
47    /** The Blade filename or view identifier used for rendering a single publication */
48    public string $detailTemplate = 'detail.blade.php';
49
50    /** The Blade filename or view identifier used for rendering the index page (or index pages, when using pagination) */
51    public string $listTemplate = 'list.blade.php';
52
53    /** The field that is used for sorting publications. */
54    public string $sortField = '__createdAt';
55
56    /** Whether the sort field should be sorted in ascending order. */
57    public bool $sortAscending = true;
58
59    /** The number of publications to show per paginated page. Set to 0 to disable pagination. */
60    public int $pageSize = 0;
61
62    /** Generic array field which can be used to store additional data as needed. */
63    public array $metadata = [];
64
65    /**
66     * The front matter fields used for the publications.
67     *
68     * @var \Illuminate\Support\Collection<string, \Hyde\Publications\Models\PublicationFieldDefinition>
69     */
70    public Collection $fields;
71
72    /** The directory of the publication files */
73    protected string $directory;
74
75    public static function get(string $name): static
76    {
77        return static::fromFile("$name/schema.json");
78    }
79
80    public static function fromFile(string $schemaFile): static
81    {
82        try {
83            return new static(...array_merge(
84                static::parseSchemaFile($schemaFile),
85                static::getRelativeDirectoryEntry($schemaFile))
86            );
87        } catch (Exception $exception) {
88            throw new RuntimeException("Could not parse schema file $schemaFile", 0, $exception);
89        }
90    }
91
92    /** @param array<array<string, string>> $fields */
93    public function __construct(
94        string $name, // todo get from directory name if not set in schema?
95        string $canonicalField = '__createdAt',
96        string $detailTemplate = 'detail.blade.php',
97        string $listTemplate = 'list.blade.php',
98        string $sortField = '__createdAt',
99        bool $sortAscending = true,
100        int $pageSize = 0,
101        array $fields = [],
102        array $metadata = [],
103        ?string $directory = null
104    ) {
105        $this->name = $name; // todo get from directory name if not set in schema?
106        $this->canonicalField = $canonicalField;
107        $this->detailTemplate = $detailTemplate;
108        $this->listTemplate = $listTemplate;
109        $this->fields = $this->parseFieldData($fields);
110        $this->directory = $directory ?? Str::slug($name);
111        $this->sortField = $sortField;
112        $this->sortAscending = $sortAscending;
113        $this->pageSize = $pageSize;
114        $this->metadata = $metadata;
115    }
116
117    public function toArray(): array
118    {
119        $array = $this->withoutNullValues([
120            'name' => $this->name,
121            'canonicalField' => $this->canonicalField,
122            'detailTemplate' => $this->detailTemplate,
123            'listTemplate' => $this->listTemplate,
124            'sortField' => $this->sortField,
125            'sortAscending' => $this->sortAscending,
126            'pageSize' => $this->pageSize,
127            'fields' => $this->fields->toArray(),
128        ]);
129
130        if ($this->metadata) {
131            $array['metadata'] = $this->metadata;
132        }
133
134        return $array;
135    }
136
137    /** @param  int  $options */
138    public function toJson($options = JSON_PRETTY_PRINT): string
139    {
140        return json_encode($this->toArray(), $options);
141    }
142
143    /** Get the publication type's identifier */
144    public function getIdentifier(): string
145    {
146        return $this->directory ?? Str::slug($this->name);
147    }
148
149    public function getSchemaFile(): string
150    {
151        return "$this->directory/schema.json";
152    }
153
154    public function getDirectory(): string
155    {
156        return $this->directory;
157    }
158
159    public function getMetadata(): array
160    {
161        return $this->metadata;
162    }
163
164    public function setMetadata(array $metadata): array
165    {
166        return $this->metadata = $metadata;
167    }
168
169    /**
170     * Get the publication fields, deserialized to PublicationFieldDefinition objects.
171     *
172     * @return \Illuminate\Support\Collection<string, \Hyde\Publications\Models\PublicationFieldDefinition>
173     */
174    public function getFields(): Collection
175    {
176        return $this->fields;
177    }
178
179    public function getFieldDefinition(string $fieldName): PublicationFieldDefinition
180    {
181        return $this->getFields()->filter(fn (PublicationFieldDefinition $field): bool => $field->name === $fieldName)->firstOrFail();
182    }
183
184    public function getCanonicalFieldDefinition(): PublicationFieldDefinition
185    {
186        if ($this->canonicalField === '__createdAt') {
187            return new PublicationFieldDefinition('string', $this->canonicalField);
188        }
189
190        return $this->getFields()->filter(fn (PublicationFieldDefinition $field): bool => $field->name === $this->canonicalField)->first();
191    }
192
193    /** @return \Illuminate\Support\Collection<\Hyde\Publications\Pages\PublicationPage> */
194    public function getPublications(): Collection
195    {
196        return Publications::getPublicationsForType($this);
197    }
198
199    public function getPaginator(int $currentPageNumber = null): Paginator
200    {
201        return new Paginator($this->getPublications(),
202            $this->pageSize,
203            $currentPageNumber,
204            $this->getIdentifier()
205        );
206    }
207
208    public function getListPage(): PublicationListPage
209    {
210        return new PublicationListPage($this);
211    }
212
213    public function usesPagination(): bool
214    {
215        return ($this->pageSize > 0) && ($this->pageSize < $this->getPublications()->count());
216    }
217
218    public function save(?string $path = null): void
219    {
220        $path ??= $this->getSchemaFile();
221        $this->needsParentDirectory($path);
222        file_put_contents(Hyde::path($path), json_encode($this->toArray(), JSON_PRETTY_PRINT));
223    }
224
225    protected static function parseSchemaFile(string $schemaFile): array
226    {
227        return json_decode(file_get_contents(Hyde::path($schemaFile)), true, 512, JSON_THROW_ON_ERROR);
228    }
229
230    protected static function getRelativeDirectoryEntry(string $schemaFile): array
231    {
232        return ['directory' => Hyde::pathToRelative(dirname($schemaFile))];
233    }
234
235    protected function parseFieldData(array $fields): Collection
236    {
237        return Collection::make($fields)->map(function (array $data): PublicationFieldDefinition {
238            return new PublicationFieldDefinition(...$data);
239        });
240    }
241
242    protected function withoutNullValues(array $array): array
243    {
244        return array_filter($array, fn (mixed $value): bool => ! is_null($value));
245    }
246
247    /**
248     * Validate the schema.json file is valid.
249     *
250     * @internal This method is experimental and may be removed without notice
251     */
252    public function validateSchemaFile(bool $throw = true): ?array
253    {
254        $method = $throw ? 'validate' : 'errors';
255
256        return PublicationSchemaValidator::call($this->getIdentifier(), $throw)->$method();
257    }
258}