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
Pull Request — master (#23)
by Roelof Jan
08:16 queued 04:41
created

Model   B

Complexity

Total Complexity 44

Size/Duplication

Total Lines 478
Duplicated Lines 0 %

Test Coverage

Coverage 97.64%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 102
c 5
b 0
f 0
dl 0
loc 478
ccs 124
cts 127
cp 0.9764
rs 8.8798
wmc 44

31 Methods

Rating   Name   Duplication   Size   Complexity  
A find() 0 7 1
A has() 0 3 1
A exists() 0 3 1
A count() 0 3 1
A matter() 0 3 1
A getFilePath() 0 9 2
A filename() 0 3 1
A rawContent() 0 9 2
A set() 0 5 1
A body() 0 5 1
A remove() 0 7 2
A get() 0 3 1
A setFileName() 0 7 1
A assertFilenameExists() 0 4 2
A getFileMatchFromDisk() 0 33 6
A addMatter() 0 3 1
A getFolderPath() 0 7 1
A getFullFileName() 0 7 2
A extension() 0 3 1
A rename() 0 11 1
A setMatter() 0 7 2
A parseFile() 0 6 1
A all() 0 4 1
A assertRequiredMatterIsPresent() 0 5 3
A rawBody() 0 3 1
A delete() 0 9 1
A __get() 0 3 1
A setExtension() 0 5 1
A save() 0 17 1
A getModelFiles() 0 12 1
A setBody() 0 5 1

How to fix   Complexity   

Complex Class

Complex classes like Model often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Model, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
4
namespace AloiaCms\Models;
5
6
use AloiaCms\Events\PostModelDeleted;
7
use AloiaCms\Events\PostModelSaved;
8
use AloiaCms\Events\PreModelDeleted;
9
use AloiaCms\Events\PreModelSaved;
10
use ContentParser\ContentParser;
11
use AloiaCms\InlineBlockParser;
12
use AloiaCms\Models\Contracts\ModelInterface;
13
use AloiaCms\Models\Contracts\StorableInterface;
14
use AloiaCms\Writer\FolderCreator;
15
use AloiaCms\Writer\FrontMatterCreator;
16
use Exception;
17
use Illuminate\Support\Arr;
18
use Illuminate\Support\Collection;
19
use Illuminate\Support\Facades\Config;
20
use Illuminate\Support\Facades\File;
21
use Spatie\YamlFrontMatter\YamlFrontMatter;
22
23
class Model implements ModelInterface, StorableInterface
24
{
25
    /**
26
     * Represents the folder name where this model saves files
27
     *
28
     * @var string $folder
29
     */
30
    protected $folder = '';
31
32
    /**
33
     * Represents the basename of the base file
34
     *
35
     * @var string|null $file_name
36
     */
37
    protected $file_name = null;
38
39
    /**
40
     * Represents the filename of the base file
41
     *
42
     * @var string|null $full_file_name
43
     */
44
    protected $full_file_name = null;
45
46
    protected $extension = 'md';
47
48
    protected $matter = [];
49
50
    protected $body = '';
51
52
    protected $required_fields = [];
53
54
    /**
55
     * Return all instances of the model
56
     *
57
     * @return Collection|ModelInterface[]
58
     */
59 7
    public static function all(): Collection
60
    {
61 7
        return Collection::make((new static())->getModelFiles())
62 7
            ->map(fn (string $filename) => self::find(pathinfo($filename, PATHINFO_FILENAME)));
63
    }
64
65
    /**
66
     * Return the amount of models of this type
67
     *
68
     * @return int
69
     */
70
    public static function count(): int
71
    {
72
        return count((new static())->getModelFiles());
73
    }
74
75
    /**
76
     * Get the folder path for this model
77
     *
78
     * @return string
79
     */
80 68
    public function getFolderPath(): string
81
    {
82 68
        $folder_path = Config::get('aloiacms.collections_path') . "/{$this->folder}";
83
84 68
        FolderCreator::forPath($folder_path);
85
86 68
        return $folder_path;
87
    }
88
89
    /**
90
     * Find a single model
91
     *
92
     * @param string $file_name
93
     * @return ModelInterface
94
     */
95 65
    public static function find(string $file_name): ModelInterface
96
    {
97 65
        $instance = new static();
98
99 65
        $instance->setFileName($file_name);
100
101 65
        return $instance;
102
    }
103
104
    /**
105
     * Set the file name for this instance
106
     *
107
     * @param string $file_name
108
     * @return ModelInterface
109
     */
110 65
    protected function setFileName(string $file_name): ModelInterface
111
    {
112 65
        $this->file_name = $file_name;
113
114 65
        $this->parseFile();
115
116 65
        return $this;
117
    }
118
119
    /**
120
     * Parse the file for this model into model variables
121
     */
122 65
    private function parseFile(): void
123
    {
124 65
        $parsed_file = YamlFrontMatter::parse($this->rawContent());
125
126 65
        $this->matter = $parsed_file->matter();
127 65
        $this->body = $parsed_file->body();
128 65
    }
129
130
    /**
131
     * Get the raw content of the file + front matter
132
     *
133
     * @return string
134
     */
135 65
    public function rawContent(): string
136
    {
137 65
        $file_path = $this->getFilePath();
138
139 65
        if ($this->exists()) {
140 48
            return file_get_contents($file_path);
141
        }
142
143 64
        return "";
144
    }
145
146
    /**
147
     * Get the file path for this instance
148
     *
149
     * @return string
150
     */
151 65
    private function getFilePath(): string
152
    {
153 65
        $folder_path = $this->getFolderPath();
154
155 65
        if (!is_null($matching_filename = $this->getFullFileName())) {
156 48
            $this->setExtension(pathinfo($matching_filename, PATHINFO_EXTENSION));
157
        }
158
159 65
        return "{$folder_path}/{$this->file_name}.{$this->extension}";
160
    }
161
162
    /**
163
     * Get full file name (including extension) for this model.
164
     *
165
     * @return string|null
166
     */
167 65
    private function getFullFileName(): ?string
168
    {
169 65
        if (!$this->full_file_name) {
170 65
            $this->full_file_name = $this->getFileMatchFromDisk();
171
        }
172
173 65
        return $this->full_file_name;
174
    }
175
176
    /**
177
     * Get the filename from disk
178
     * This uses the least amount of loops possible.
179
     *
180
     * @return string|null
181
     */
182 65
    private function getFileMatchFromDisk(): ?string
183
    {
184 65
        $haystack = $this->getModelFiles();
185
186 65
        $min = 0;
187 65
        $max = count($haystack);
188
189
        // No saved files, lookup is pointless
190 65
        if ($max === 0) {
191 64
            return null;
192
        }
193
194 48
        while ($max >= $min) {
195 48
            $mid = floor(($min + $max) / 2);
196
197
            // Current key doesn't exist, so let's try a lower number
198 48
            if (!isset($haystack[$mid])) {
199 2
                $max = $mid - 1;
200 2
                continue;
201
            }
202
203 48
            if (strpos($haystack[$mid], "{$this->file_name}.") !== false) {
204 48
                return $haystack[$mid];
205 6
            } elseif ($haystack[$mid] < $this->file_name) {
206
                // The new chunk will be the second half
207 2
                $min = $mid + 1;
208
            } else {
209
                // The new chunk will be the first half
210 5
                $max = $mid - 1;
211
            }
212
        }
213
214 6
        return null;
215
    }
216
217
    /**
218
     * Get all models for this type
219
     *
220
     * @return array
221
     */
222 65
    private function getModelFiles(): array
223
    {
224 65
        $filenames = array_values(
225
            array_diff(
226 65
                scandir($this->getFolderPath()),
0 ignored issues
show
Bug introduced by
It seems like scandir($this->getFolderPath()) can also be of type false; however, parameter $array1 of array_diff() does only seem to accept array, 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

226
                /** @scrutinizer ignore-type */ scandir($this->getFolderPath()),
Loading history...
227 65
                ['..', '.']
228
            )
229
        );
230
231 65
        sort($filenames);
232
233 65
        return $filenames;
234
    }
235
236
    /**
237
     * Set the file extension
238
     *
239
     * @param string $extension
240
     * @return ModelInterface
241
     */
242 48
    public function setExtension(string $extension): ModelInterface
243
    {
244 48
        $this->extension = $extension;
245
246 48
        return $this;
247
    }
248
249
    /**
250
     * Determine whether the current model exists
251
     *
252
     * @return bool
253
     */
254 65
    public function exists(): bool
255
    {
256 65
        return !is_null($this->getFullFileName());
257
    }
258
259
    /**
260
     * Rename this file to the given name
261
     *
262
     * @param string $new_name
263
     * @return Model
264
     */
265 1
    public function rename(string $new_name): ModelInterface
266
    {
267 1
        $old_file_path = $this->getFilePath();
268
269 1
        $this->file_name = $new_name;
270
271 1
        $new_file_path = $this->getFilePath();
272
273 1
        File::move($old_file_path, $new_file_path);
274
275 1
        return self::find($new_name);
276
    }
277
278
    /**
279
     * Save this instance to file
280
     *
281
     * @return ModelInterface
282
     * @throws Exception
283
     */
284 54
    public function save(): ModelInterface
285
    {
286 54
        PreModelSaved::dispatch($this);
287
288 54
        $file_content = FrontMatterCreator::seed($this->matter, $this->body)->create();
289
290 54
        $this->assertFilenameExists();
291
292 53
        $this->assertRequiredMatterIsPresent();
293
294 51
        $file_path = $this->getFilePath();
295
296 51
        file_put_contents($file_path, $file_content);
297
298 51
        PostModelSaved::dispatch($this);
299
300 51
        return $this;
301
    }
302
303
    /**
304
     * Throw exception when the file name is not set for this instance
305
     *
306
     * @throws Exception
307
     */
308 54
    private function assertFilenameExists()
309
    {
310 54
        if (is_null($this->file_name)) {
311 1
            throw new Exception("Filename is required");
312
        }
313 53
    }
314
315
    /**
316
     * Throw exception if at least one required matter attribute is not present
317
     *
318
     * @throws Exception
319
     */
320 53
    private function assertRequiredMatterIsPresent()
321
    {
322 53
        foreach ($this->required_fields as $required_field) {
323 45
            if (!isset($this->matter[$required_field])) {
324 2
                throw new Exception("Attribute {$required_field} is required");
325
            }
326
        }
327 51
    }
328
329
    /**
330
     * Get the front matter
331
     *
332
     * @return array
333
     */
334 2
    public function matter(): array
335
    {
336 2
        return $this->matter;
337
    }
338
339
    /**
340
     * Set a value on the specified key in the configuration
341
     *
342
     * Kept around for backward compatibility
343
     *
344
     * @param string $key
345
     * @param $value
346
     * @return ModelInterface
347
     *
348
     * @deprecated since 3.2.0
349
     */
350 1
    public function addMatter(string $key, $value): ModelInterface
351
    {
352 1
        return $this->set($key, $value);
353
    }
354
355
    /**
356
     * Set a value on the specified key in the configuration
357
     *
358
     * @param string $key
359
     * @param $value
360
     * @return $this|ModelInterface
361
     */
362 3
    public function set(string $key, $value): ModelInterface
363
    {
364 3
        $this->matter[$key] = $value;
365
366 3
        return $this;
367
    }
368
369
    /**
370
     * Set data in the front matter, but only for the keys specified in the input array
371
     *
372
     * @param array $matter
373
     * @return ModelInterface
374
     */
375 46
    public function setMatter(array $matter): ModelInterface
376
    {
377 46
        foreach (array_keys($matter) as $key) {
378 46
            $this->matter[$key] = $matter[$key];
379
        }
380
381 46
        return $this;
382
    }
383
384
    /**
385
     * Remove a key from the configuration
386
     *
387
     * @param string $key
388
     * @return $this|ModelInterface
389
     */
390 2
    public function remove(string $key): ModelInterface
391
    {
392 2
        if ($this->has($key)) {
393 2
            unset($this->matter[$key]);
394
        }
395
396 2
        return $this;
397
    }
398
399
    /**
400
     * Determine whether a key is present in the configuration
401
     *
402
     * @param string $key
403
     * @return bool
404
     */
405 2
    public function has(string $key): bool
406
    {
407 2
        return isset($this->matter[$key]);
408
    }
409
410
    /**
411
     * Get the parse file body
412
     *
413
     * @return string
414
     */
415 19
    public function body(): string
416
    {
417 19
        $content = new ContentParser($this->rawBody(), $this->extension());
418
419 19
        return (new InlineBlockParser)->parseHtmlString($content->parse());
420
    }
421
422
    /**
423
     * Get the raw file body
424
     *
425
     * @return string
426
     */
427 21
    public function rawBody(): string
428
    {
429 21
        return $this->body;
430
    }
431
432
    /**
433
     * Get the file extension
434
     *
435
     * @return string
436
     */
437 20
    public function extension(): string
438
    {
439 20
        return $this->extension;
440
    }
441
442
    /**
443
     * Set the file body
444
     *
445
     * @param string $body
446
     * @return ModelInterface
447
     */
448 38
    public function setBody(string $body): ModelInterface
449
    {
450 38
        $this->body = $body;
451
452 38
        return $this;
453
    }
454
455
    /**
456
     * Get the file name for this instance
457
     *
458
     * @return string
459
     */
460 1
    public function filename(): ?string
461
    {
462 1
        return $this->file_name;
463
    }
464
465
    /**
466
     * Delete the current model
467
     *
468
     * @return bool
469
     */
470 3
    public function delete(): bool
471
    {
472 3
        PreModelDeleted::dispatch($this);
473
474 3
        $is_successful = File::delete($this->getFilePath());
475
476 3
        PostModelDeleted::dispatch($this);
477
478 3
        return $is_successful;
479
    }
480
481
    /**
482
     * Get front matter information through an accessor
483
     *
484
     * @param $key
485
     * @return mixed|null
486
     */
487 12
    public function __get($key)
488
    {
489 12
        return $this->get($key);
490
    }
491
492
    /**
493
     * Get the value of the specified key, return null if it doesn't exist
494
     *
495
     * @param string $key
496
     * @return mixed|null
497
     */
498 15
    public function get(string $key)
499
    {
500 15
        return $this->matter[$key] ?? null;
501
    }
502
}
503