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
22:25
created

FileOverrider::sortFiles()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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

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

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

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

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