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 ( de1180...132a25 )
by Roelof Jan
03:35 queued 12s
created

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

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

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

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

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