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 ( b3ada7...02540b )
by Roelof Jan
03:53 queued 13s
created

Model::getFilePath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 4
nc 2
nop 0
dl 0
loc 9
ccs 5
cts 5
cp 1
crap 2
rs 10
c 1
b 0
f 0
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\Contracts\Routing\UrlRoutable;
21
use Illuminate\Support\Collection;
22
use Illuminate\Support\Facades\Config;
23
use Illuminate\Support\Facades\File;
24
use Illuminate\Support\Str;
25
use Spatie\YamlFrontMatter\YamlFrontMatter;
26
27
class Model implements ModelInterface, StorableInterface, UrlRoutable
28
{
29
    /**
30
     * Represents the folder name where this model saves files
31
     *
32
     * @var string $folder
33
     */
34
    protected $folder;
35
36
    /**
37
     * Represents the basename of the base file
38
     *
39
     * @var string|null $file_name
40
     */
41
    protected $file_name = null;
42
43
    /**
44
     * Represents the filename of the base file
45
     *
46
     * @var string|null $full_file_name
47
     */
48
    protected $full_file_name = null;
49
50
    protected $extension = 'md';
51
52
    protected $matter = [];
53
54
    protected $body = '';
55 7
56
    protected $required_fields = [];
57 7
58 7
    /**
59
     * Return all instances of the model
60
     *
61
     * @return Collection|ModelInterface[]
62
     */
63
    public static function all(): Collection
64
    {
65
        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

65
        return Collection::make(/** @scrutinizer ignore-type */ (new static())->getModelFiles())
Loading history...
66
            ->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

66
            ->map(fn (string $filename) => self::find(/** @scrutinizer ignore-type */ pathinfo($filename, PATHINFO_FILENAME)));
Loading history...
67
    }
68
69
    /**
70
     * Return the amount of models of this type
71
     *
72
     * @return int
73
     */
74
    public static function count(): int
75
    {
76 66
        return count((new static())->getModelFiles());
77
    }
78 66
79
    /**
80 66
     * Guess the folder name for this model
81
     *
82 66
     * @return string
83
     */
84
    protected function guessFolder(): string
85
    {
86
        return $this->folder ?? Str::snake(Str::pluralStudly(class_basename($this)));
87
    }
88
89
    /**
90
     * Get the folder path for this model
91 63
     *
92
     * @return string
93 63
     */
94
    public function getFolderPath(): string
95 63
    {
96
        $folder = $this->guessFolder();
97 63
98
        $full_folder_path = Config::get('aloiacms.collections_path') . "/{$folder}";
99
100
        FolderCreator::forPath($full_folder_path);
101
102
        return $full_folder_path;
103
    }
104
105
    /**
106 63
     * Find a single model from an instance of the model
107
     *
108 63
     * @param string $file_name
109
     * @return ModelInterface
110 63
     */
111
    public function findById(string $file_name): ModelInterface
112 63
    {
113
        $instance = new static();
114
115
        $instance->setFileName($file_name);
116
117
        return $instance;
118 63
    }
119
120 63
    /**
121
     * Find a single model
122 63
     *
123 63
     * @param string $file_name
124 63
     * @return ModelInterface
125
     */
126
    public static function find(string $file_name): ModelInterface
127
    {
128
        $instance = new static();
129
130
        $instance->setFileName($file_name);
131 63
132
        return $instance;
133 63
    }
134
135 63
    /**
136 47
     * Set the file name for this instance
137
     *
138
     * @param string $file_name
139 62
     * @return ModelInterface
140
     */
141
    protected function setFileName(string $file_name): ModelInterface
142
    {
143
        $this->file_name = $file_name;
144
145
        $this->parseFile();
146
147 63
        return $this;
148
    }
149 63
150
    /**
151 63
     * Parse the file for this model into model variables
152 47
     */
153
    private function parseFile(): void
154
    {
155 63
        $parsed_file = YamlFrontMatter::parse($this->rawContent());
156
157
        $this->matter = $parsed_file->matter();
158
        $this->body = $parsed_file->body();
159
    }
160
161
    /**
162
     * Get the raw content of the file + front matter
163 63
     *
164
     * @return string
165 63
     */
166 63
    public function rawContent(): string
167
    {
168
        $file_path = $this->getFilePath();
169 63
170
        if ($this->exists()) {
171
            return file_get_contents($file_path);
172
        }
173
174
        return "";
175
    }
176
177
    /**
178 63
     * Get the file path for this instance
179
     *
180 63
     * @return string
181
     */
182 63
    private function getFilePath(): string
183 63
    {
184
        $folder_path = $this->getFolderPath();
185
186 63
        if (!is_null($matching_filename = $this->getFullFileName())) {
187 62
            $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

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

327
            return self::find(/** @scrutinizer ignore-type */ $old_name);
Loading history...
328 2
        }
329
    }
330
331
    /**
332
     * Save this instance to file
333
     *
334
     * @return ModelInterface
335
     * @throws Exception
336
     */
337
    public function save(): ModelInterface
338
    {
339
        PreModelSaved::dispatch($this);
340
341
        $file_content = FrontMatterCreator::seed($this->matter, $this->body)->create();
342 1
343
        $this->assertFilenameExists();
344 1
345
        $this->assertRequiredMatterIsPresent();
346
347
        $file_path = $this->getFilePath();
348
349
        file_put_contents($file_path, $file_content);
350
351
        PostModelSaved::dispatch($this);
352
353
        return $this;
354 3
    }
355
356 3
    /**
357
     * Throw exception when the file name is not set for this instance
358 3
     *
359
     * @throws Exception
360
     */
361
    private function assertFilenameExists()
362
    {
363
        if (is_null($this->file_name)) {
364
            throw new Exception("Filename is required");
365
        }
366
    }
367 46
368
    /**
369 46
     * Throw exception if at least one required matter attribute is not present
370 46
     *
371
     * @throws Exception
372
     */
373 46
    private function assertRequiredMatterIsPresent()
374
    {
375
        foreach ($this->required_fields as $required_field) {
376
            if (!isset($this->matter[$required_field])) {
377
                throw new Exception("Attribute {$required_field} is required");
378
            }
379
        }
380
    }
381
382 2
    /**
383
     * Get the front matter
384 2
     *
385 2
     * @return array
386
     */
387
    public function matter(): array
388 2
    {
389
        return $this->matter;
390
    }
391
392
    /**
393
     * Set a value on the specified key in the configuration
394
     *
395
     * Kept around for backward compatibility
396
     *
397 2
     * @param string $key
398
     * @param $value
399 2
     * @return ModelInterface
400
     *
401
     * @deprecated since 3.2.0
402
     */
403
    public function addMatter(string $key, $value): ModelInterface
404
    {
405
        return $this->set($key, $value);
406
    }
407 19
408
    /**
409 19
     * Set a value on the specified key in the configuration
410
     *
411 19
     * @param string $key
412
     * @param $value
413
     * @return $this|ModelInterface
414
     */
415
    public function set(string $key, $value): ModelInterface
416
    {
417
        $this->matter[$key] = $value;
418
419 21
        return $this;
420
    }
421 21
422
    /**
423
     * Set data in the front matter, but only for the keys specified in the input array
424
     *
425
     * @param array $matter
426
     * @return ModelInterface
427
     */
428
    public function setMatter(array $matter): ModelInterface
429 20
    {
430
        foreach (array_keys($matter) as $key) {
431 20
            $this->matter[$key] = $matter[$key];
432
        }
433
434
        return $this;
435
    }
436
437
    /**
438
     * Remove a key from the configuration
439
     *
440 38
     * @param string $key
441
     * @return $this|ModelInterface
442 38
     */
443
    public function remove(string $key): ModelInterface
444 38
    {
445
        if ($this->has($key)) {
446
            unset($this->matter[$key]);
447
        }
448
449
        return $this;
450
    }
451
452 1
    /**
453
     * Determine whether a key is present in the configuration
454 1
     *
455
     * @param string $key
456
     * @return bool
457
     */
458
    public function has(string $key): bool
459
    {
460
        return isset($this->matter[$key]);
461
    }
462 2
463
    /**
464 2
     * Get the parse file body
465
     *
466
     * @return string
467
     */
468
    public function body(): string
469
    {
470
        $content = new ContentParser($this->rawBody(), $this->extension());
471
472
        return (new InlineBlockParser)->parseHtmlString($content->parse());
473 12
    }
474
475 12
    /**
476
     * Get the raw file body
477
     *
478
     * @return string
479
     */
480
    public function rawBody(): string
481
    {
482
        return $this->body;
483
    }
484 15
485
    /**
486 15
     * Get the file extension
487
     *
488
     * @return string
489
     */
490
    public function extension(): string
491
    {
492
        return $this->extension;
493
    }
494
495
    /**
496
     * Set the file body
497
     *
498
     * @param string $body
499
     * @return ModelInterface
500
     */
501
    public function setBody(string $body): ModelInterface
502
    {
503
        $this->body = $body;
504
505
        return $this;
506
    }
507
508
    /**
509
     * Get the file name for this instance
510
     *
511
     * @return string
512
     */
513
    public function filename(): ?string
514
    {
515
        return $this->file_name;
516
    }
517
518
    /**
519
     * Delete the current model
520
     *
521
     * @return bool
522
     */
523
    public function delete(): bool
524
    {
525
        PreModelDeleted::dispatch($this);
526
527
        $is_successful = File::delete($this->getFilePath());
528
529
        PostModelDeleted::dispatch($this);
530
531
        return $is_successful;
532
    }
533
534
    /**
535
     * Get the value of the model's route key.
536
     *
537
     * @return mixed
538
     */
539
    public function getRouteKey()
540
    {
541
        return $this->{$this->getRouteKeyName()};
542
    }
543
544
    /**
545
     * Get the route key for the model.
546
     *
547
     * @return string
548
     */
549
    public function getRouteKeyName()
550
    {
551
        return 'file_name';
552
    }
553
554
    /**
555
     * Retrieve the model for a bound value.
556
     *
557
     * @param  mixed  $value
558
     * @param  string|null  $field
559
     * @return \Illuminate\Database\Eloquent\Model|null
560
     */
561
    public function resolveRouteBinding($value, $field = null)
562
    {
563
        $model = $this->find($value);
564
565
        return $model->exists() ? $model : null;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $model->exists() ? $model : null also could return the type AloiaCms\Models\Model which is incompatible with the documented return type Illuminate\Database\Eloquent\Model|null.
Loading history...
566
    }
567
568
    /**
569
     * Retrieve the child model for a bound value.
570
     *
571
     * @param  string  $childType
572
     * @param  mixed  $value
573
     * @param  string|null  $field
574
     * @return \Illuminate\Database\Eloquent\Model|null
575
     */
576
    public function resolveChildRouteBinding($childType, $value, $field)
577
    {
578
        throw new \BadMethodCallException('Method not implemented.');
579
    }
580
581
    /**
582
     * Get front matter information through an accessor
583
     *
584
     * @param $key
585
     * @return mixed|null
586
     */
587
    public function __get($key)
588
    {
589
        return $this->get($key);
590
    }
591
592
    /**
593
     * Get the value of the specified key, return null if it doesn't exist
594
     *
595
     * @param string $key
596
     * @return mixed|null
597
     */
598
    public function get(string $key, $default = null)
599
    {
600
        return $this->matter[$key] ?? $default;
601
    }
602
}
603