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 (#199)
by joseph
21:04
created

FileOverrider::compareOverridesWithProject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 28
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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

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

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

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

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