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 ( 4b5b9f...48d107 )
by Roelof Jan
03:03 queued 13s
created

Model::__set()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
c 0
b 0
f 0
dl 0
loc 6
ccs 1
cts 1
cp 1
rs 10
cc 2
nc 2
nop 2
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\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
     * Use a magic setter for convenient attribute setting.
410
     *
411 19
     * @param string $key
412
     * @param $value
413
     * @return void
414
     */
415
    public function __set(string $key, $value)
416
    {
417
        if ($key == "body") {
418
            $this->setBody($value);
419 21
        } else {
420
            $this->set($key, $value);
421 21
        }
422
    }
423
424
    /**
425
     * Set a value on the specified key in the configuration
426
     *
427
     * @param string $key
428
     * @param $value
429 20
     * @return $this|ModelInterface
430
     */
431 20
    public function set(string $key, $value): ModelInterface
432
    {
433
        $this->matter[$key] = $value;
434
435
        return $this;
436
    }
437
438
    /**
439
     * Set data in the front matter, but only for the keys specified in the input array
440 38
     *
441
     * @param array $matter
442 38
     * @return ModelInterface
443
     */
444 38
    public function setMatter(array $matter): ModelInterface
445
    {
446
        foreach (array_keys($matter) as $key) {
447
            $this->matter[$key] = $matter[$key];
448
        }
449
450
        return $this;
451
    }
452 1
453
    /**
454 1
     * Remove a key from the configuration
455
     *
456
     * @param string $key
457
     * @return $this|ModelInterface
458
     */
459
    public function remove(string $key): ModelInterface
460
    {
461
        if ($this->has($key)) {
462 2
            unset($this->matter[$key]);
463
        }
464 2
465
        return $this;
466
    }
467
468
    /**
469
     * Determine whether a key is present in the configuration
470
     *
471
     * @param string $key
472
     * @return bool
473 12
     */
474
    public function has(string $key): bool
475 12
    {
476
        return isset($this->matter[$key]);
477
    }
478
479
    /**
480
     * Get the parse file body
481
     *
482
     * @return string
483
     */
484 15
    public function body(): string
485
    {
486 15
        $content = new ContentParser($this->rawBody(), $this->extension());
487
488
        return (new InlineBlockParser)->parseHtmlString($content->parse());
489
    }
490
491
    /**
492
     * Get the raw file body
493
     *
494
     * @return string
495
     */
496
    public function rawBody(): string
497
    {
498
        return $this->body;
499
    }
500
501
    /**
502
     * Get the file extension
503
     *
504
     * @return string
505
     */
506
    public function extension(): string
507
    {
508
        return $this->extension;
509
    }
510
511
    /**
512
     * Set the file body
513
     *
514
     * @param string $body
515
     * @return ModelInterface
516
     */
517
    public function setBody(string $body): ModelInterface
518
    {
519
        $this->body = $body;
520
521
        return $this;
522
    }
523
524
    /**
525
     * Get the file name for this instance
526
     *
527
     * @return string
528
     */
529
    public function filename(): ?string
530
    {
531
        return $this->file_name;
532
    }
533
534
    /**
535
     * Delete the current model
536
     *
537
     * @return bool
538
     */
539
    public function delete(): bool
540
    {
541
        PreModelDeleted::dispatch($this);
542
543
        $is_successful = File::delete($this->getFilePath());
544
545
        PostModelDeleted::dispatch($this);
546
547
        return $is_successful;
548
    }
549
550
    /**
551
     * Get the value of the model's route key.
552
     *
553
     * @return mixed
554
     */
555
    public function getRouteKey()
556
    {
557
        return $this->{$this->getRouteKeyName()};
558
    }
559
560
    /**
561
     * Get the route key for the model.
562
     *
563
     * @return string
564
     */
565
    public function getRouteKeyName()
566
    {
567
        return 'file_name';
568
    }
569
570
    /**
571
     * Retrieve the model for a bound value.
572
     *
573
     * @param  mixed  $value
574
     * @param  string|null  $field
575
     * @return \Illuminate\Database\Eloquent\Model|null
576
     */
577
    public function resolveRouteBinding($value, $field = null)
578
    {
579
        $model = $this->find($value);
580
581
        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...
582
    }
583
584
    /**
585
     * Retrieve the child model for a bound value.
586
     *
587
     * @param  string  $childType
588
     * @param  mixed  $value
589
     * @param  string|null  $field
590
     * @return \Illuminate\Database\Eloquent\Model|null
591
     */
592
    public function resolveChildRouteBinding($childType, $value, $field)
593
    {
594
        throw new \BadMethodCallException('Method not implemented.');
595
    }
596
597
    /**
598
     * Get front matter information through an accessor
599
     *
600
     * @param $key
601
     * @return mixed|null
602
     */
603
    public function __get($key)
604
    {
605
        return $this->get($key);
606
    }
607
608
    /**
609
     * Get the value of the specified key, return null if it doesn't exist
610
     *
611
     * @param string $key
612
     * @return mixed|null
613
     */
614
    public function get(string $key, $default = null)
615
    {
616
        return $this->matter[$key] ?? $default;
617
    }
618
}
619