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

FileOverrider::getProjectFileHash()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1.037

Importance

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

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

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

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

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