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.
Completed
Pull Request — master (#224)
by joseph
19:55
created

FileOverrider   C

Complexity

Total Complexity 56

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 9
Bugs 0 Features 0
Metric Value
eloc 202
c 9
b 0
f 0
dl 0
loc 455
rs 5.5199
ccs 0
cts 318
cp 0
wmc 56

27 Methods

Rating   Name   Duplication   Size   Complexity  
A getPathToProjectRoot() 0 3 1
A recreateOverride() 0 12 1
A getDiff() 0 19 1
A checkForDuplicateOverrides() 0 16 2
A sortFilesByKey() 0 5 1
A getRealPath() 0 11 4
A getOverrideDirectoryForFile() 0 8 4
A projectFileIsSameAsOverride() 0 6 1
A setPathToOverridesDirectory() 0 5 1
A setPathToProjectRoot() 0 6 1
A getRelativePathToFile() 0 3 1
A compareOverridesWithProject() 0 28 4
A getRelativePathInProjectFromOverridePath() 0 10 1
A getOverrideForPath() 0 14 3
A getInvalidOverrides() 0 25 4
A overrideFileHashIsCorrect() 0 10 2
A updateOverrideFiles() 0 23 3
A __construct() 0 8 2
A getFileNameNoExtensionForPathInProject() 0 5 1
A getProjectFileHash() 0 3 1
A getFileHash() 0 5 1
A sortFiles() 0 5 1
A createNewOverride() 0 18 3
A getOverridesIterator() 0 31 4
A cleanPath() 0 3 1
A getPathToOverridesDirectory() 0 3 1
B applyOverrides() 0 31 6

How to fix   Complexity   

Complex Class

Complex classes like FileOverrider 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 FileOverrider, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
namespace EdmondsCommerce\DoctrineStaticMeta\CodeGeneration\PostProcessor;
6
7
use Generator;
8
use RecursiveDirectoryIterator;
9
use RecursiveIteratorIterator;
10
use RuntimeException;
11
use SebastianBergmann\Diff\Differ;
12
use SebastianBergmann\Diff\Output\DiffOnlyOutputBuilder;
13
use SplFileInfo;
14
use function copy;
15
use function dirname;
16
use function realpath;
17
18
/**
19
 * This class provides the necessary functionality to allow you to maintain a set of file overrides and to safely apply
20
 * them as part of a post process to your main build process
21
 * @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
22
 */
23
class FileOverrider
24
{
25
    /**
26
     * The default path to the overrides folder, relative to the project root
27
     */
28
    public const OVERRIDES_PATH = '/build/overrides';
29
30
    private const EXTENSION_LENGTH_NO_HASH_IN_PROJECT     = 4;
31
    private const EXTENSION_LENGTH_WITH_HASH_IN_OVERRIDES = 46;
32
    private const OVERRIDE_EXTENSION                      = 'override';
33
34
    /**
35
     * @var string
36
     */
37
    private $pathToProjectRoot;
38
    /**
39
     * @var string
40
     */
41
    private $pathToOverridesDirectory;
42
    /**
43
     * @var Differ
44
     */
45
    private $differ;
46
47
    public function __construct(
48
        string $pathToProjectRoot = null
49
    ) {
50
        if (null !== $pathToProjectRoot) {
51
            $this->setPathToProjectRoot($pathToProjectRoot);
52
        }
53
        $builder = new DiffOnlyOutputBuilder('');
54
        $this->differ = new Differ($builder);
55
    }
56
57
    /**
58
     * @return string
59
     */
60
    public function getPathToProjectRoot(): string
61
    {
62
        return $this->pathToProjectRoot;
63
    }
64
65
    /**
66
     * @param string $pathToProjectRoot
67
     *
68
     * @return $this
69
     * @throws RuntimeException
70
     */
71
    public function setPathToProjectRoot(string $pathToProjectRoot): self
72
    {
73
        $this->pathToProjectRoot = $this->getRealPath($pathToProjectRoot);
74
        $this->setPathToOverridesDirectory($this->pathToProjectRoot . self::OVERRIDES_PATH);
75
76
        return $this;
77
    }
78
79
    public function recreateOverride(string $relativePathToFileInOverrides): array
80
    {
81
        $overridePath = $this->cleanPath($this->pathToProjectRoot . '/' . $relativePathToFileInOverrides);
82
83
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($overridePath);
84
85
        $old = $relativePathToFileInOverrides . '-old';
86
        rename($overridePath, $overridePath . '-old');
87
88
        $new = $this->createNewOverride($this->pathToProjectRoot . '/' . $relativePathToFileInProject);
89
90
        return [$old, $new];
91
    }
92
93
    private function cleanPath(string $path): string
94
    {
95
        return preg_replace('%/{2,}%', '/', $path);
96
    }
97
98
    private function getRelativePathInProjectFromOverridePath(string $pathToFileInOverrides): string
99
    {
100
        $pathToFileInOverrides = $this->cleanPath($pathToFileInOverrides);
101
        $relativePath          = substr($pathToFileInOverrides, strlen($this->getPathToOverridesDirectory()));
102
        $relativeDir           = dirname($relativePath);
103
        $filename              = basename($pathToFileInOverrides);
104
        $filename              = substr($filename, 0, -self::EXTENSION_LENGTH_WITH_HASH_IN_OVERRIDES) . '.php';
105
106
        return $this->getRelativePathToFile(
107
            $this->getRealPath($this->pathToProjectRoot . '/' . $relativeDir . '/' . $filename)
108
        );
109
    }
110
111
    /**
112
     * @return string
113
     */
114
    public function getPathToOverridesDirectory(): string
115
    {
116
        return $this->getRealPath($this->pathToOverridesDirectory);
117
    }
118
119
    /**
120
     * @param string $pathToOverridesDirectory
121
     *
122
     * @return FileOverrider
123
     */
124
    public function setPathToOverridesDirectory(string $pathToOverridesDirectory): FileOverrider
125
    {
126
        $this->pathToOverridesDirectory = $this->getRealPath($pathToOverridesDirectory);
127
128
        return $this;
129
    }
130
131
    private function getRealPath(string $path): string
132
    {
133
        $realPath = realpath($path);
134
        if (false === $realPath) {
135
            if (!mkdir($path, 0777, true) && !is_dir($path)) {
136
                throw new RuntimeException(sprintf('Directory "%s" was not created', $path));
137
            }
138
            $realPath = realpath($path);
139
        }
140
141
        return $realPath;
142
    }
143
144
    private function getRelativePathToFile(string $pathToFileInProject): string
145
    {
146
        return str_replace($this->pathToProjectRoot, '', $this->getRealPath($pathToFileInProject));
147
    }
148
149
    /**
150
     * Create a new Override File by copying the file from the project into the project's overrides directory
151
     *
152
     * @param string $pathToFileInProject
153
     *
154
     * @return string
155
     */
156
    public function createNewOverride(string $pathToFileInProject): string
157
    {
158
        $relativePathToFileInProject = $this->getRelativePathToFile($pathToFileInProject);
159
        if (null !== $this->getOverrideForPath($relativePathToFileInProject)) {
160
            throw new RuntimeException('Override already exists for path ' . $relativePathToFileInProject);
161
        }
162
        $overridePath        =
163
            $this->getOverrideDirectoryForFile($relativePathToFileInProject) .
164
            '/' . $this->getFileNameNoExtensionForPathInProject($relativePathToFileInProject) .
165
            '.' . $this->getProjectFileHash($relativePathToFileInProject) .
166
            '.php.override';
167
        $pathToFileInProject = $this->pathToProjectRoot . '/' . $relativePathToFileInProject;
168
        if (false === is_file($pathToFileInProject)) {
169
            throw new RuntimeException('path ' . $pathToFileInProject . ' is not a file');
170
        }
171
        copy($pathToFileInProject, $overridePath);
172
173
        return $this->getRelativePathToFile($overridePath);
174
    }
175
176
    private function getOverrideForPath(string $relativePathToFileInProject): ?string
177
    {
178
        $fileDirectory       = $this->getOverrideDirectoryForFile($relativePathToFileInProject);
179
        $fileNameNoExtension = $this->getFileNameNoExtensionForPathInProject($relativePathToFileInProject);
180
        $filesInDirectory    = glob("$fileDirectory/$fileNameNoExtension*" . self::OVERRIDE_EXTENSION);
181
        if ([] === $filesInDirectory) {
182
            return null;
183
        }
184
        if (1 === count($filesInDirectory)) {
0 ignored issues
show
Bug introduced by
It seems like $filesInDirectory can also be of type false; however, parameter $var of count() does only seem to accept Countable|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

184
        if (1 === count(/** @scrutinizer ignore-type */ $filesInDirectory)) {
Loading history...
185
            return $fileDirectory . '/' . current($filesInDirectory);
0 ignored issues
show
Bug introduced by
It seems like $filesInDirectory can also be of type false; however, parameter $array of current() 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

185
            return $fileDirectory . '/' . current(/** @scrutinizer ignore-type */ $filesInDirectory);
Loading history...
186
        }
187
        throw new RuntimeException(
188
            'Found more than one override in path ' . $fileDirectory . ': '
189
            . print_r($filesInDirectory, true)
190
        );
191
    }
192
193
    private function getOverrideDirectoryForFile(string $relativePathToFileInProject): string
194
    {
195
        $path = $this->getPathToOverridesDirectory() . dirname($relativePathToFileInProject);
196
        if (!is_dir($path) && !(mkdir($path, 0777, true) && is_dir($path))) {
197
            throw new RuntimeException('Failed making override directory path ' . $path);
198
        }
199
200
        return $this->getRealPath($path);
201
    }
202
203
    private function getFileNameNoExtensionForPathInProject(string $relativePathToFileInProject): string
204
    {
205
        $fileName = basename($relativePathToFileInProject);
206
207
        return substr($fileName, 0, -self::EXTENSION_LENGTH_NO_HASH_IN_PROJECT);
208
    }
209
210
    private function getProjectFileHash(string $relativePathToFileInProject): string
211
    {
212
        return $this->getFileHash($this->pathToProjectRoot . '/' . $relativePathToFileInProject);
213
    }
214
215
    private function getFileHash(string $path): string
216
    {
217
        $contents = \ts\file_get_contents($path);
218
219
        return md5($contents);
220
    }
221
222
    /**
223
     * Loop over all the override files and update with the file contents from the project
224
     *
225
     * @param array|null $toUpdateRelativePathToFilesInProject
226
     *
227
     * @return array[] the file paths that have been updated
228
     */
229
    public function updateOverrideFiles(array $toUpdateRelativePathToFilesInProject): array
230
    {
231
        $filesUpdated = [];
232
        $filesSkipped = [];
233
        [$filesDifferent, $filesSame] = $this->compareOverridesWithProject();
234
235
        foreach ($filesDifferent as $fileDifferent) {
236
            $relativePathToFileInOverrides = $fileDifferent['overridePath'];
237
            $relativePathToFileInProject   = $fileDifferent['projectPath'];
238
            if (false === isset($toUpdateRelativePathToFilesInProject[$relativePathToFileInProject])) {
239
                $filesSkipped[] = $relativePathToFileInProject;
240
                continue;
241
            }
242
            $pathToFileInProject   = $this->pathToProjectRoot . $relativePathToFileInProject;
243
            $pathToFileInOverrides = $this->pathToProjectRoot . $relativePathToFileInOverrides;
244
            copy($pathToFileInProject, $pathToFileInOverrides);
245
            $filesUpdated[] = $relativePathToFileInProject;
246
        }
247
248
        return [
249
            $this->sortFiles($filesUpdated),
250
            $this->sortFiles($filesSkipped),
251
            $this->sortFiles($filesSame),
252
        ];
253
    }
254
255
    public function compareOverridesWithProject(): array
256
    {
257
        $fileSame       = [];
258
        $filesDifferent = [];
259
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
260
            $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
261
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
262
                $fileSame[] = $relativePathToFileInProject;
263
                continue;
264
            }
265
            $pathToFileInProject = $this->pathToProjectRoot . $relativePathToFileInProject;
266
            if (false === is_file($pathToFileInProject)) {
267
                throw new RuntimeException(
268
                    'path ' . $pathToFileInProject
269
                    . ' is not a file, the override should probably be removed, unless something else has gone wrong?'
270
                );
271
            }
272
            $relativePathToFileInOverrides = $this->getRelativePathToFile($pathToFileInOverrides);
273
274
            $filesDifferent[$relativePathToFileInProject]['overridePath'] = $relativePathToFileInOverrides;
275
            $filesDifferent[$relativePathToFileInProject]['projectPath']  = $relativePathToFileInProject;
276
            $filesDifferent[$relativePathToFileInProject]['diff']         = $this->getDiff(
277
                $relativePathToFileInProject,
278
                $relativePathToFileInOverrides
279
            );
280
        }
281
282
        return [$this->sortFilesByKey($filesDifferent), $this->sortFiles($fileSame)];
283
    }
284
285
    /**
286
     * Yield file paths in the override folder
287
     *
288
     * @return Generator|string[]
289
     */
290
    private function getOverridesIterator(): Generator
291
    {
292
        try {
293
            $recursiveIterator = new RecursiveIteratorIterator(
294
                new RecursiveDirectoryIterator(
295
                    $this->getPathToOverridesDirectory(),
296
                    RecursiveDirectoryIterator::SKIP_DOTS
297
                ),
298
                RecursiveIteratorIterator::SELF_FIRST
299
            );
300
            foreach ($recursiveIterator as $fileInfo) {
301
                /**
302
                 * @var SplFileInfo $fileInfo
303
                 */
304
                if ($fileInfo->isFile()) {
305
                    if (
306
                        self::OVERRIDE_EXTENSION !== substr(
307
                            $fileInfo->getFilename(),
308
                            -strlen(self::OVERRIDE_EXTENSION)
309
                        )
310
                    ) {
311
                        continue;
312
                    }
313
                    $overridesPath = $fileInfo->getPathname();
314
                    $this->checkForDuplicateOverrides($overridesPath);
315
                    yield $overridesPath;
316
                }
317
            }
318
        } finally {
319
            $recursiveIterator = null;
320
            unset($recursiveIterator);
321
        }
322
    }
323
324
    private function checkForDuplicateOverrides(string $overridesPath): void
325
    {
326
        $overridesPathNoExtension = substr(
327
            $overridesPath,
328
            0,
329
            -self::EXTENSION_LENGTH_WITH_HASH_IN_OVERRIDES
330
        );
331
332
        $glob = glob($overridesPathNoExtension . '*.' . self::OVERRIDE_EXTENSION);
333
        if (count($glob) > 1) {
0 ignored issues
show
Bug introduced by
It seems like $glob can also be of type false; however, parameter $var of count() does only seem to accept Countable|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

333
        if (count(/** @scrutinizer ignore-type */ $glob) > 1) {
Loading history...
334
            $glob    = array_map('basename', $glob);
0 ignored issues
show
Bug introduced by
It seems like $glob can also be of type false; however, parameter $arr1 of array_map() 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

334
            $glob    = array_map('basename', /** @scrutinizer ignore-type */ $glob);
Loading history...
335
            $dirname = dirname($overridesPathNoExtension);
336
            throw new RuntimeException(
337
                "Found duplicated overrides in:\n\n$dirname\n\n"
338
                . print_r($glob, true)
339
                . "\n\nYou need to fix this so that there is only one override"
340
            );
341
        }
342
    }
343
344
    /**
345
     * Is the file in the project the same as the override file already?
346
     *
347
     * @param string $pathToFileInOverrides
348
     *
349
     * @return bool
350
     */
351
    private function projectFileIsSameAsOverride(string $pathToFileInOverrides): bool
352
    {
353
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
354
355
        return $this->getFileHash($this->pathToProjectRoot . '/' . $relativePathToFileInProject) ===
356
               $this->getFileHash($pathToFileInOverrides);
357
    }
358
359
    private function getDiff(
360
        string $relativePathToFileInProject,
361
        string $relativePathToFileInOverrides
362
    ): string {
363
        $diff = $this->differ->diff(
364
            \ts\file_get_contents($this->pathToProjectRoot . '/' . $relativePathToFileInOverrides),
365
            \ts\file_get_contents($this->pathToProjectRoot . '/' . $relativePathToFileInProject)
366
        );
367
368
        return <<<TEXT
369
370
-------------------------------------------------------------------------
371
372
Diff between:
373
374
+++ Project:  $relativePathToFileInProject
375
--- Override: $relativePathToFileInOverrides
376
 
377
$diff
378
379
-------------------------------------------------------------------------
380
381
TEXT;
382
    }
383
384
    private function sortFilesByKey(array $files): array
385
    {
386
        ksort($files, SORT_STRING);
387
388
        return $files;
389
    }
390
391
    private function sortFiles(array $files): array
392
    {
393
        sort($files, SORT_STRING);
394
395
        return $files;
396
    }
397
398
    /**
399
     * Before applying overrides, we can check for errors and then return useful information
400
     *
401
     * @return array
402
     */
403
    public function getInvalidOverrides(): array
404
    {
405
        $errors = [];
406
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
407
            if ($this->overrideFileHashIsCorrect($pathToFileInOverrides)) {
408
                continue;
409
            }
410
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
411
                continue;
412
            }
413
            $relativePathToFileInOverrides = $this->getRelativePathToFile($pathToFileInOverrides);
414
            $relativePathToFileInProject   =
415
                $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
416
417
            $errors[$relativePathToFileInOverrides]['overridePath'] = $relativePathToFileInOverrides;
418
            $errors[$relativePathToFileInOverrides]['projectPath']  = $relativePathToFileInProject;
419
            $errors[$relativePathToFileInOverrides]['diff']         = $this->getDiff(
420
                $relativePathToFileInProject,
421
                $relativePathToFileInOverrides
422
            );
423
            $errors[$relativePathToFileInOverrides]['new md5']      =
424
                $this->getProjectFileHash($relativePathToFileInProject);
425
        }
426
427
        return $errors;
428
    }
429
430
    private function overrideFileHashIsCorrect(string $pathToFileInOverrides): bool
431
    {
432
        $filenameParts = explode('.', basename($pathToFileInOverrides));
433
        if (4 !== count($filenameParts)) {
434
            throw new RuntimeException('Invalid override filename ' . $pathToFileInOverrides);
435
        }
436
        $hash                        = $filenameParts[1];
437
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
438
439
        return $hash === $this->getProjectFileHash($relativePathToFileInProject);
440
    }
441
442
    /**
443
     * Loop over all the override files and copy into the project
444
     *
445
     * @return array[] the file paths that have been updated
446
     */
447
    public function applyOverrides(): array
448
    {
449
        $filesUpdated = [];
450
        $filesSame    = [];
451
        $errors       = [];
452
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
453
            $relativePathToFileInProject   = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
454
            $relativePathToFileInOverrides = $this->getRelativePathToFile($pathToFileInOverrides);
455
            if ($this->overrideFileHashIsCorrect($pathToFileInOverrides)) {
456
                if (false === is_file($pathToFileInOverrides)) {
457
                    throw new RuntimeException('path ' . $pathToFileInOverrides . ' is not a file');
458
                }
459
                copy($pathToFileInOverrides, $this->pathToProjectRoot . $relativePathToFileInProject);
460
                $filesUpdated[] = $relativePathToFileInProject;
461
                continue;
462
            }
463
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
464
                $filesSame[] = $relativePathToFileInProject;
465
                continue;
466
            }
467
            $errors[$pathToFileInOverrides]['diff']    = $this->getDiff(
468
                $relativePathToFileInProject,
469
                $relativePathToFileInOverrides
470
            );
471
            $errors[$pathToFileInOverrides]['new md5'] = $this->getProjectFileHash($relativePathToFileInProject);
472
        }
473
        if ([] !== $errors) {
474
            throw new RuntimeException('These file hashes were not up to date:' . print_r($errors, true));
475
        }
476
477
        return [$this->sortFiles($filesUpdated), $this->sortFiles($filesSame)];
478
    }
479
}
480