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.

Issues (32)

src/Models/Model.php (5 issues)

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
    protected $has_changes = false;
59
60
    /**
61
     * Return all instances of the model
62
     *
63
     * @return Collection|ModelInterface[]
64
     */
65
    public static function all(): Collection
66
    {
67
        return Collection::make((new static())->getModelFiles())
0 ignored issues
show
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

67
        return Collection::make(/** @scrutinizer ignore-type */ (new static())->getModelFiles())
Loading history...
68
            ->map(fn (string $filename) => self::find(pathinfo($filename, PATHINFO_FILENAME)));
0 ignored issues
show
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

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

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

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