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.

FileOverrider   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 455
Duplicated Lines 0 %

Test Coverage

Coverage 52.83%

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 168
cts 318
cp 0.5283
wmc 56

27 Methods

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

185
        if (1 === count(/** @scrutinizer ignore-type */ $filesInDirectory)) {
Loading history...
186 1
            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

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

334
        if (count(/** @scrutinizer ignore-type */ $glob) > 1) {
Loading history...
335
            $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

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