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
19:04
created

FileOverrider::applyOverrides()   B

Complexity

Conditions 6
Paths 9

Size

Total Lines 30
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 42

Importance

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

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

163
            return $fileDirectory . '/' . current(/** @scrutinizer ignore-type */ $filesInDirectory);
Loading history...
164
        }
165
        throw new \RuntimeException(
166
            'Found more than one override in path ' . $fileDirectory . ': '
167
            . print_r($filesInDirectory, true)
168
        );
169
    }
170
171
    private function getOverrideDirectoryForFile(string $relativePathToFileInProject): string
172
    {
173
        $path = $this->getPathToOverridesDirectory() . \dirname($relativePathToFileInProject);
174
        if (!is_dir($path) && !(mkdir($path, 0777, true) && is_dir($path))) {
175
            throw new \RuntimeException('Failed making override directory path ' . $path);
176
        }
177
178
        return $this->getRealPath($path);
179
    }
180
181
    private function getFileNameNoExtensionForPathInProject(string $relativePathToFileInProject): string
182
    {
183
        $fileName = basename($relativePathToFileInProject);
184
185
        return substr($fileName, 0, -self::EXTENSION_LENGTH_NO_HASH_IN_PROJECT);
186
    }
187
188
    private function getProjectFileHash(string $relativePathToFileInProject): string
189
    {
190
        return $this->getFileHash($this->pathToProjectRoot . '/' . $relativePathToFileInProject);
191
    }
192
193
    private function getFileHash(string $path): string
194
    {
195
        $contents = \ts\file_get_contents($path);
196
197
        return md5($contents);
198
    }
199
200
    /**
201
     * Loop over all the override files and update with the file contents from the project
202
     *
203
     * @return array[] the file paths that have been updated
204
     */
205
    public function updateOverrideFiles(): array
206
    {
207
        $filesUpdated = [];
208
        $fileSame     = [];
209
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
210
            $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
211
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
212
                $fileSame[] = $relativePathToFileInProject;
213
                continue;
214
            }
215
            $pathToFileInProject = $this->pathToProjectRoot . $relativePathToFileInProject;
216
            if (false === is_file($pathToFileInProject)) {
217
                throw new \RuntimeException('path ' . $pathToFileInProject . ' is not a file');
218
            }
219
            copy($pathToFileInProject, $pathToFileInOverrides);
220
            $filesUpdated[] = $relativePathToFileInProject;
221
        }
222
223
        return [$this->sortFiles($filesUpdated), $this->sortFiles($fileSame)];
224
    }
225
226
    /**
227
     * Yield file paths in the override folder
228
     *
229
     * @return \Generator|string[]
230
     */
231
    private function getOverridesIterator(): \Generator
232
    {
233
        try {
234
            $recursiveIterator = new \RecursiveIteratorIterator(
235
                new \RecursiveDirectoryIterator(
236
                    $this->getPathToOverridesDirectory(),
237
                    \RecursiveDirectoryIterator::SKIP_DOTS
238
                ),
239
                \RecursiveIteratorIterator::SELF_FIRST
240
            );
241
            foreach ($recursiveIterator as $fileInfo) {
242
                /**
243
                 * @var \SplFileInfo $fileInfo
244
                 */
245
                if ($fileInfo->isFile()) {
246
                    if (self::OVERRIDE_EXTENSION !== substr(
247
                            $fileInfo->getFilename(),
248
                            -strlen(self::OVERRIDE_EXTENSION)
249
                        )
250
                    ) {
251
                        continue;
252
                    }
253
                    yield $fileInfo->getPathname();
254
                }
255
            }
256
        } finally {
257
            $recursiveIterator = null;
258
            unset($recursiveIterator);
259
        }
260
    }
261
262
    /**
263
     * Is the file in the project the same as the override file already?
264
     *
265
     * @param string $pathToFileInOverrides
266
     *
267
     * @return bool
268
     */
269
    private function projectFileIsSameAsOverride(string $pathToFileInOverrides): bool
270
    {
271
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
272
273
        return $this->getFileHash($this->pathToProjectRoot . '/' . $relativePathToFileInProject) ===
274
               $this->getFileHash($pathToFileInOverrides);
275
    }
276
277
    private function sortFiles(array $files): array
278
    {
279
        sort($files, SORT_STRING);
280
281
        return $files;
282
    }
283
284
    /**
285
     * Before applying overrides, we can check for errors and then return useful information
286
     *
287
     * @return array
288
     */
289
    public function getInvalidOverrides(): array
290
    {
291
        $builder = new DiffOnlyOutputBuilder(
292
            "--- Original\n+++ New\n"
293
        );
294
        $differ  = new Differ($builder);
295
        $errors  = [];
296
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
297
            if ($this->overrideFileHashIsCorrect($pathToFileInOverrides)) {
298
                continue;
299
            }
300
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
301
                continue;
302
            }
303
            $relativePathToOverride      = $this->getRelativePathToFile($pathToFileInOverrides);
304
            $relativePathToFileInProject =
305
                $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
306
307
            $errors[$relativePathToOverride]['overridePath'] = $relativePathToOverride;
308
            $errors[$relativePathToOverride]['projectPath']  = $relativePathToFileInProject;
309
            $errors[$relativePathToOverride]['diff']         = $differ->diff(
310
                \ts\file_get_contents($this->pathToProjectRoot . $relativePathToFileInProject),
311
                \ts\file_get_contents($pathToFileInOverrides)
312
            );
313
            $errors[$relativePathToOverride]['new md5']      =
314
                $this->getProjectFileHash($relativePathToFileInProject);
315
        }
316
317
        return $errors;
318
    }
319
320
    private function overrideFileHashIsCorrect(string $pathToFileInOverrides): bool
321
    {
322
        $filenameParts = explode('.', basename($pathToFileInOverrides));
323
        if (4 !== count($filenameParts)) {
324
            throw new \RuntimeException('Invalid override filename ' . $pathToFileInOverrides);
325
        }
326
        $hash                        = $filenameParts[1];
327
        $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
328
329
        return $hash === $this->getProjectFileHash($relativePathToFileInProject);
330
    }
331
332
    /**
333
     * Loop over all the override files and copy into the project
334
     *
335
     * @return array[] the file paths that have been updated
336
     */
337
    public function applyOverrides(): array
338
    {
339
        $filesUpdated = [];
340
        $filesSame    = [];
341
        $errors       = [];
342
        foreach ($this->getOverridesIterator() as $pathToFileInOverrides) {
343
            $relativePathToFileInProject = $this->getRelativePathInProjectFromOverridePath($pathToFileInOverrides);
344
            if ($this->overrideFileHashIsCorrect($pathToFileInOverrides)) {
345
                if (false === is_file($pathToFileInOverrides)) {
346
                    throw new \RuntimeException('path ' . $pathToFileInOverrides . ' is not a file');
347
                }
348
                copy($pathToFileInOverrides, $this->pathToProjectRoot . $relativePathToFileInProject);
349
                $filesUpdated[] = $relativePathToFileInProject;
350
                continue;
351
            }
352
            if ($this->projectFileIsSameAsOverride($pathToFileInOverrides)) {
353
                $filesSame[] = $relativePathToFileInProject;
354
                continue;
355
            }
356
            $errors[$pathToFileInOverrides]['diff']    = $differ->diff(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $differ seems to be never defined.
Loading history...
357
                \ts\file_get_contents($this->pathToProjectRoot . $relativePathToFileInProject),
358
                \ts\file_get_contents($pathToFileInOverrides)
359
            );
360
            $errors[$pathToFileInOverrides]['new md5'] = $this->getProjectFileHash($relativePathToFileInProject);
361
        }
362
        if ([] !== $errors) {
363
            throw new \RuntimeException('These file hashes were not up to date:' . print_r($errors, true));
364
        }
365
366
        return [$this->sortFiles($filesUpdated), $this->sortFiles($filesSame)];
367
    }
368
}
369