GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Test Failed
Push — master ( 825fca...40a20f )
by Roelof Jan
04:11 queued 12s
created

Model::addMatter()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 3
rs 10
ccs 0
cts 0
cp 0
crap 2
1
<?php
2
3
4
namespace AloiaCms\Models;
5
6
use AloiaCms\Events\ModelRenameFailed;
7
use AloiaCms\Events\PostModelDeleted;
8
use AloiaCms\Events\PostModelRenamed;
9
use AloiaCms\Events\PostModelSaved;
10
use AloiaCms\Events\PreModelDeleted;
11
use AloiaCms\Events\PreModelRenamed;
12
use AloiaCms\Events\PreModelSaved;
13
use ContentParser\ContentParser;
14
use AloiaCms\InlineBlockParser;
15
use AloiaCms\Models\Contracts\ModelInterface;
16
use AloiaCms\Models\Contracts\StorableInterface;
17
use AloiaCms\Writer\FolderCreator;
18
use AloiaCms\Writer\FrontMatterCreator;
19
use Exception;
20
use Illuminate\Support\Collection;
21
use Illuminate\Support\Facades\Config;
22
use Illuminate\Support\Facades\File;
23
use Spatie\YamlFrontMatter\YamlFrontMatter;
24
25
class Model implements ModelInterface, StorableInterface
26
{
27
    /**
28
     * Represents the folder name where this model saves files
29
     *
30
     * @var string $folder
31
     */
32
    protected $folder = '';
33
34
    /**
35
     * Represents the basename of the base file
36
     *
37
     * @var string|null $file_name
38
     */
39
    protected $file_name = null;
40
41
    /**
42
     * Represents the filename of the base file
43
     *
44
     * @var string|null $full_file_name
45
     */
46
    protected $full_file_name = null;
47
48
    protected $extension = 'md';
49
50
    protected $matter = [];
51
52
    protected $body = '';
53
54
    protected $required_fields = [];
55 7
56
    /**
57 7
     * Return all instances of the model
58 7
     *
59
     * @return Collection|ModelInterface[]
60
     */
61
    public static function all(): Collection
62
    {
63
        return Collection::make((new static())->getModelFiles())
0 ignored issues
show
Bug introduced by
new static()->getModelFiles() of type array is incompatible with the type Illuminate\Contracts\Support\Arrayable expected by parameter $items of Illuminate\Support\Collection::make(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

63
        return Collection::make(/** @scrutinizer ignore-type */ (new static())->getModelFiles())
Loading history...
64
            ->map(fn (string $filename) => self::find(pathinfo($filename, PATHINFO_FILENAME)));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($filename, Aloi...dels\PATHINFO_FILENAME) can also be of type array; however, parameter $file_name of AloiaCms\Models\Model::find() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

64
            ->map(fn (string $filename) => self::find(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_FILENAME)));
Loading history...
65
    }
66
67
    /**
68
     * Return the amount of models of this type
69
     *
70
     * @return int
71
     */
72
    public static function count(): int
73
    {
74
        return count((new static())->getModelFiles());
75
    }
76 66
77
    /**
78 66
     * Get the folder path for this model
79
     *
80 66
     * @return string
81
     */
82 66
    public function getFolderPath(): string
83
    {
84
        $folder_path = Config::get('aloiacms.collections_path') . "/{$this->folder}";
85
86
        FolderCreator::forPath($folder_path);
87
88
        return $folder_path;
89
    }
90
91 63
    /**
92
     * Find a single model from an instance of the model
93 63
     *
94
     * @param string $file_name
95 63
     * @return ModelInterface
96
     */
97 63
    public function findById(string $file_name): ModelInterface
98
    {
99
        $instance = new static();
100
101
        $instance->setFileName($file_name);
102
103
        return $instance;
104
    }
105
106 63
    /**
107
     * Find a single model
108 63
     *
109
     * @param string $file_name
110 63
     * @return ModelInterface
111
     */
112 63
    public static function find(string $file_name): ModelInterface
113
    {
114
        $instance = new static();
115
116
        $instance->setFileName($file_name);
117
118 63
        return $instance;
119
    }
120 63
121
    /**
122 63
     * Set the file name for this instance
123 63
     *
124 63
     * @param string $file_name
125
     * @return ModelInterface
126
     */
127
    protected function setFileName(string $file_name): ModelInterface
128
    {
129
        $this->file_name = $file_name;
130
131 63
        $this->parseFile();
132
133 63
        return $this;
134
    }
135 63
136 47
    /**
137
     * Parse the file for this model into model variables
138
     */
139 62
    private function parseFile(): void
140
    {
141
        $parsed_file = YamlFrontMatter::parse($this->rawContent());
142
143
        $this->matter = $parsed_file->matter();
144
        $this->body = $parsed_file->body();
145
    }
146
147 63
    /**
148
     * Get the raw content of the file + front matter
149 63
     *
150
     * @return string
151 63
     */
152 47
    public function rawContent(): string
153
    {
154
        $file_path = $this->getFilePath();
155 63
156
        if ($this->exists()) {
157
            return file_get_contents($file_path);
158
        }
159
160
        return "";
161
    }
162
163 63
    /**
164
     * Get the file path for this instance
165 63
     *
166 63
     * @return string
167
     */
168
    private function getFilePath(): string
169 63
    {
170
        $folder_path = $this->getFolderPath();
171
172
        if (!is_null($matching_filename = $this->getFullFileName())) {
173
            $this->setExtension(pathinfo($matching_filename, PATHINFO_EXTENSION));
0 ignored issues
show
Bug introduced by
It seems like pathinfo($matching_filen...els\PATHINFO_EXTENSION) can also be of type array; however, parameter $extension of AloiaCms\Models\Model::setExtension() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

173
            $this->setExtension(/** @scrutinizer ignore-type */ pathinfo($matching_filename, PATHINFO_EXTENSION));
Loading history...
174
        }
175
176
        return "{$folder_path}/{$this->file_name}.{$this->extension}";
177
    }
178 63
179
    /**
180 63
     * Get full file name (including extension) for this model.
181
     *
182 63
     * @return string|null
183 63
     */
184
    private function getFullFileName(): ?string
185
    {
186 63
        if (!$this->full_file_name) {
187 62
            $this->full_file_name = $this->getFileMatchFromDisk();
188
        }
189
190 47
        return $this->full_file_name;
191 47
    }
192
193
    /**
194 47
     * Get the filename from disk
195 2
     * This uses the least amount of loops possible.
196 2
     *
197
     * @return string|null
198
     */
199 47
    private function getFileMatchFromDisk(): ?string
200 47
    {
201 6
        $haystack = $this->getModelFiles();
202
203 2
        $min = 0;
204
        $max = count($haystack);
205
206 5
        // No saved files, lookup is pointless
207
        if ($max === 0) {
208
            return null;
209
        }
210 6
211
        while ($max >= $min) {
212
            $mid = floor(($min + $max) / 2);
213
214
            // Current key doesn't exist, so let's try a lower number
215
            if (!isset($haystack[$mid])) {
216
                $max = $mid - 1;
217
                continue;
218 63
            }
219
220 63
            if (strpos($haystack[$mid], "{$this->file_name}.") !== false) {
221
                return $haystack[$mid];
222 63
            } elseif ($haystack[$mid] < $this->file_name) {
223 63
                // The new chunk will be the second half
224
                $min = $mid + 1;
225
            } else {
226
                // The new chunk will be the first half
227 63
                $max = $mid - 1;
228
            }
229 63
        }
230
231
        return null;
232
    }
233
234
    /**
235
     * Get all models for this type
236
     *
237
     * @return array
238 47
     */
239
    private function getModelFiles(): array
240 47
    {
241
        // Filter out any:
242 47
        // - hidden files: files with leading .
243
        // - current folder: .
244
        // - parent folder: ..
245
246
        $filenames = array_values(
247
            array_filter(
248
                array_diff(
249
                    scandir($this->getFolderPath()),
250 63
                    ['..', '.']
251
                ),
252 63
                fn (string $filename) => $filename[0] !== "."
253
            )
254
        );
255
256
        sort($filenames);
257
258
        return $filenames;
259
    }
260
261 1
    /**
262
     * Set the file extension
263 1
     *
264
     * @param string $extension
265 1
     * @return ModelInterface
266
     */
267 1
    public function setExtension(string $extension): ModelInterface
268
    {
269 1
        $this->extension = $extension;
270
271 1
        return $this;
272
    }
273
274
    /**
275
     * Determine whether the current model exists
276
     *
277
     * @return bool
278
     */
279
    public function exists(): bool
280 52
    {
281
        return file_exists($this->getFilePath());
282 52
    }
283
284 52
    /**
285
     * Rename this file to the given name
286 51
     *
287
     * @param string $new_name
288 49
     * @return Model
289
     */
290 49
    public function rename(string $new_name): ModelInterface
291
    {
292 49
        $old_file_path = $this->getFilePath();
293
        $old_name = $this->filename();
294
295
        PreModelRenamed::dispatch($this, $new_name);
296
297
        $this->file_name = $new_name;
298
299
        $new_file_path = $this->getFilePath();
300 52
301
        try {
302 52
            $is_moved = File::move($old_file_path, $new_file_path);
303 1
304
            if (!$is_moved) {
305 51
                throw new \InvalidArgumentException("Could not move the file");
306
            } else {
307
                PostModelRenamed::dispatch($this, $new_name);
308
                return self::find($new_name);
309
            }
310
        } catch (\Exception $exception) {
311
            $this->file_name = $old_name;
312 51
            ModelRenameFailed::dispatch($this, $new_name);
313
            return self::find($old_name);
0 ignored issues
show
Bug introduced by
It seems like $old_name can also be of type null; however, parameter $file_name of AloiaCms\Models\Model::find() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

313
            return self::find(/** @scrutinizer ignore-type */ $old_name);
Loading history...
314 51
        }
315 43
    }
316 2
317
    /**
318
     * Save this instance to file
319 49
     *
320
     * @return ModelInterface
321
     * @throws Exception
322
     */
323
    public function save(): ModelInterface
324
    {
325
        PreModelSaved::dispatch($this);
326 2
327
        $file_content = FrontMatterCreator::seed($this->matter, $this->body)->create();
328 2
329
        $this->assertFilenameExists();
330
331
        $this->assertRequiredMatterIsPresent();
332
333
        $file_path = $this->getFilePath();
334
335
        file_put_contents($file_path, $file_content);
336
337
        PostModelSaved::dispatch($this);
338
339
        return $this;
340
    }
341
342 1
    /**
343
     * Throw exception when the file name is not set for this instance
344 1
     *
345
     * @throws Exception
346
     */
347
    private function assertFilenameExists()
348
    {
349
        if (is_null($this->file_name)) {
350
            throw new Exception("Filename is required");
351
        }
352
    }
353
354 3
    /**
355
     * Throw exception if at least one required matter attribute is not present
356 3
     *
357
     * @throws Exception
358 3
     */
359
    private function assertRequiredMatterIsPresent()
360
    {
361
        foreach ($this->required_fields as $required_field) {
362
            if (!isset($this->matter[$required_field])) {
363
                throw new Exception("Attribute {$required_field} is required");
364
            }
365
        }
366
    }
367 46
368
    /**
369 46
     * Get the front matter
370 46
     *
371
     * @return array
372
     */
373 46
    public function matter(): array
374
    {
375
        return $this->matter;
376
    }
377
378
    /**
379
     * Set a value on the specified key in the configuration
380
     *
381
     * Kept around for backward compatibility
382 2
     *
383
     * @param string $key
384 2
     * @param $value
385 2
     * @return ModelInterface
386
     *
387
     * @deprecated since 3.2.0
388 2
     */
389
    public function addMatter(string $key, $value): ModelInterface
390
    {
391
        return $this->set($key, $value);
392
    }
393
394
    /**
395
     * Set a value on the specified key in the configuration
396
     *
397 2
     * @param string $key
398
     * @param $value
399 2
     * @return $this|ModelInterface
400
     */
401
    public function set(string $key, $value): ModelInterface
402
    {
403
        $this->matter[$key] = $value;
404
405
        return $this;
406
    }
407 19
408
    /**
409 19
     * Set data in the front matter, but only for the keys specified in the input array
410
     *
411 19
     * @param array $matter
412
     * @return ModelInterface
413
     */
414
    public function setMatter(array $matter): ModelInterface
415
    {
416
        foreach (array_keys($matter) as $key) {
417
            $this->matter[$key] = $matter[$key];
418
        }
419 21
420
        return $this;
421 21
    }
422
423
    /**
424
     * Remove a key from the configuration
425
     *
426
     * @param string $key
427
     * @return $this|ModelInterface
428
     */
429 20
    public function remove(string $key): ModelInterface
430
    {
431 20
        if ($this->has($key)) {
432
            unset($this->matter[$key]);
433
        }
434
435
        return $this;
436
    }
437
438
    /**
439
     * Determine whether a key is present in the configuration
440 38
     *
441
     * @param string $key
442 38
     * @return bool
443
     */
444 38
    public function has(string $key): bool
445
    {
446
        return isset($this->matter[$key]);
447
    }
448
449
    /**
450
     * Get the parse file body
451
     *
452 1
     * @return string
453
     */
454 1
    public function body(): string
455
    {
456
        $content = new ContentParser($this->rawBody(), $this->extension());
457
458
        return (new InlineBlockParser)->parseHtmlString($content->parse());
459
    }
460
461
    /**
462 2
     * Get the raw file body
463
     *
464 2
     * @return string
465
     */
466
    public function rawBody(): string
467
    {
468
        return $this->body;
469
    }
470
471
    /**
472
     * Get the file extension
473 12
     *
474
     * @return string
475 12
     */
476
    public function extension(): string
477
    {
478
        return $this->extension;
479
    }
480
481
    /**
482
     * Set the file body
483
     *
484 15
     * @param string $body
485
     * @return ModelInterface
486 15
     */
487
    public function setBody(string $body): ModelInterface
488
    {
489
        $this->body = $body;
490
491
        return $this;
492
    }
493
494
    /**
495
     * Get the file name for this instance
496
     *
497
     * @return string
498
     */
499
    public function filename(): ?string
500
    {
501
        return $this->file_name;
502
    }
503
504
    /**
505
     * Delete the current model
506
     *
507
     * @return bool
508
     */
509
    public function delete(): bool
510
    {
511
        PreModelDeleted::dispatch($this);
512
513
        $is_successful = File::delete($this->getFilePath());
514
515
        PostModelDeleted::dispatch($this);
516
517
        return $is_successful;
518
    }
519
520
    /**
521
     * Get front matter information through an accessor
522
     *
523
     * @param $key
524
     * @return mixed|null
525
     */
526
    public function __get($key)
527
    {
528
        return $this->get($key);
529
    }
530
531
    /**
532
     * Get the value of the specified key, return null if it doesn't exist
533
     *
534
     * @param string $key
535
     * @return mixed|null
536
     */
537
    public function get(string $key)
538
    {
539
        return $this->matter[$key] ?? null;
540
    }
541
}
542