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
Push — master ( 2a6b46...0d82f1 )
by joseph
20s queued 14s
created

FileOverrider::checkForDuplicateOverrides()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 16
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2.6067

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 11
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 16
rs 9.9
ccs 7
cts 15
cp 0.4667
crap 2.6067
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
        string $relativePathToOverridesDirectory = self::OVERRIDES_PATH
51
    ) {
52 5
        if (null !== $pathToProjectRoot) {
53 5
            $this->setPathToProjectRoot($pathToProjectRoot);
54 5
            $this->setPathToOverridesDirectory($this->pathToProjectRoot . '/' . $relativePathToOverridesDirectory);
55
        }
56 5
        $builder      = new DiffOnlyOutputBuilder('');
57 5
        $this->differ = new Differ($builder);
58 5
    }
59
60
    /**
61
     * @return string
62
     */
63
    public function getPathToProjectRoot(): string
64
    {
65
        return $this->pathToProjectRoot;
66
    }
67
68
    /**
69
     * @param string $pathToProjectRoot
70
     *
71
     * @return $this
72
     * @throws RuntimeException
73
     */
74 5
    public function setPathToProjectRoot(string $pathToProjectRoot): self
75
    {
76 5
        $this->pathToProjectRoot = $this->getRealPath($pathToProjectRoot);
77 5
        $this->setPathToOverridesDirectory($this->pathToProjectRoot . self::OVERRIDES_PATH);
78
79 5
        return $this;
80
    }
81
82
    public function recreateOverride(string $relativePathToFileInOverrides): array
83
    {
84
        $overridePath = $this->cleanPath($this->pathToProjectRoot . '/' . $relativePathToFileInOverrides);
85
86
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($overridePath);
87
88
        $old = $relativePathToFileInOverrides . '-old';
89
        rename($overridePath, $overridePath . '-old');
90
91
        $new = $this->createNewOverride($this->pathToProjectRoot . '/' . $relativePathToFileInProject);
92
93
        return [$old, $new];
94
    }
95
96 3
    private function cleanPath(string $path): string
97
    {
98 3
        return preg_replace('%/{2,}%', '/', $path);
99
    }
100
101 3
    private function getRelativePathInProjectFromOverridePath(string $pathToFileInOverrides): string
102
    {
103 3
        $pathToFileInOverrides = $this->cleanPath($pathToFileInOverrides);
104 3
        $relativePath          = substr($pathToFileInOverrides, strlen($this->getPathToOverridesDirectory()));
105 3
        $relativeDir           = dirname($relativePath);
106 3
        $filename              = basename($pathToFileInOverrides);
107 3
        $filename              = substr($filename, 0, -self::EXTENSION_LENGTH_WITH_HASH_IN_OVERRIDES) . '.php';
108
109 3
        return $this->getRelativePathToFile(
110 3
            $this->getRealPath($this->pathToProjectRoot . '/' . $relativeDir . '/' . $filename)
111
        );
112
    }
113
114
    /**
115
     * @return string
116
     */
117 5
    public function getPathToOverridesDirectory(): string
118
    {
119 5
        return $this->getRealPath($this->pathToOverridesDirectory);
120
    }
121
122
    /**
123
     * @param string $pathToOverridesDirectory
124
     *
125
     * @return FileOverrider
126
     */
127 5
    public function setPathToOverridesDirectory(string $pathToOverridesDirectory): FileOverrider
128
    {
129 5
        $this->pathToOverridesDirectory = $this->getRealPath($pathToOverridesDirectory);
130
131 5
        return $this;
132
    }
133
134 5
    private function getRealPath(string $path): string
135
    {
136 5
        $realPath = realpath($path);
137 5
        if (false === $realPath) {
138
            if (!mkdir($path, 0777, true) && !is_dir($path)) {
139
                throw new RuntimeException(sprintf('Directory "%s" was not created', $path));
140
            }
141
            $realPath = realpath($path);
142
        }
143
144 5
        return $realPath;
145
    }
146
147 5
    private function getRelativePathToFile(string $pathToFileInProject): string
148
    {
149 5
        return str_replace($this->pathToProjectRoot, '', $this->getRealPath($pathToFileInProject));
150
    }
151
152
    /**
153
     * Create a new Override File by copying the file from the project into the project's overrides directory
154
     *
155
     * @param string $pathToFileInProject
156
     *
157
     * @return string
158
     */
159 2
    public function createNewOverride(string $pathToFileInProject): string
160
    {
161 2
        $relativePathToFileInProject = $this->getRelativePathToFile($pathToFileInProject);
162 2
        if (null !== $this->getOverrideForPath($relativePathToFileInProject)) {
163 1
            throw new RuntimeException('Override already exists for path ' . $relativePathToFileInProject);
164
        }
165
        $overridePath        =
166 1
            $this->getOverrideDirectoryForFile($relativePathToFileInProject) .
167 1
            '/' . $this->getFileNameNoExtensionForPathInProject($relativePathToFileInProject) .
168 1
            '.' . $this->getProjectFileHash($relativePathToFileInProject) .
169 1
            '.php.override';
170 1
        $pathToFileInProject = $this->pathToProjectRoot . '/' . $relativePathToFileInProject;
171 1
        if (false === is_file($pathToFileInProject)) {
172
            throw new RuntimeException('path ' . $pathToFileInProject . ' is not a file');
173
        }
174 1
        copy($pathToFileInProject, $overridePath);
175
176 1
        return $this->getRelativePathToFile($overridePath);
177
    }
178
179 2
    private function getOverrideForPath(string $relativePathToFileInProject): ?string
180
    {
181 2
        $fileDirectory       = $this->getOverrideDirectoryForFile($relativePathToFileInProject);
182 2
        $fileNameNoExtension = $this->getFileNameNoExtensionForPathInProject($relativePathToFileInProject);
183 2
        $filesInDirectory    = glob("$fileDirectory/$fileNameNoExtension*" . self::OVERRIDE_EXTENSION);
184 2
        if ([] === $filesInDirectory) {
185 1
            return null;
186
        }
187 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

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

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

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

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