Completed
Push — master ( 57b1db...16daf9 )
by Mike
03:22
created

LocalUpgradePackages::getUpgradeChains()   D

Complexity

Conditions 18
Paths 27

Size

Total Lines 98
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 18
eloc 52
nc 27
nop 3
dl 0
loc 98
rs 4.7996
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Sugarcrm\UpgradeSpec\Data\Provider\SourceCode;
4
5
use Sugarcrm\UpgradeSpec\Data\Exception\WrongProviderException;
6
use Sugarcrm\UpgradeSpec\Context\Upgrade;
7
use Symfony\Component\Finder\Finder;
8
9
class LocalUpgradePackages implements SourceCodeProviderInterface
10
{
11
    /**
12
     * @var null|array
13
     */
14
    private $suitablePackages = null;
15
16
    /**
17
     * Gets the list of potentially broken customizations (changed and deleted files)
18
     *
19
     * @param Upgrade $context
20
     *
21
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
22
     */
23
    public function getPotentiallyBrokenCustomizations(Upgrade $context)
24
    {
25
        $packages = $this->getSuitablePackages($context);
26
27
        return $this->getChangedFiles('/Users/m.kamornikov/Dev/sugarcrm/build/rome/builds/ult/sugarcrm', $packages);
28
    }
29
30
    /**
31
     * Gets the lists of upgrade steps for the given source
32
     *
33
     * @param Upgrade $context
34
     *
35
     * @return mixed
36
     * @throws WrongProviderException
37
     */
38
    public function getUpgradeSteps(Upgrade $context)
39
    {
40
        if (file_exists($context->getTargetPath() . '/.git')) {
41
            throw new WrongProviderException('This provider uses zipped upgrade packages as source');
42
        }
43
44
        return $this->getSuitablePackages($context);
45
    }
46
47
    /**
48
     * @param $buildPath
49
     * @param $packages
50
     *
51
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string,array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
52
     */
53
    private function getChangedFiles($buildPath, $packages)
54
    {
55
        $modifiedFiles = $deletedFiles = $packageZips = [];
56
57
        foreach ($packages as $package) {
58
            $zip = new \ZipArchive();
59
            if (!$zip->open($package)) {
60
                throw new \RuntimeException(sprintf('Can\'t open zip archive: %s', $package));
61
            }
62
63
            $packageZips[$package] = $zip;
64
65
            eval(str_replace(['<?php', '<?', '?>'], '', $zip->getFromName(basename($package, '.zip') . DS . 'files.md5')));
0 ignored issues
show
Coding Style introduced by
It is generally not recommended to use eval unless absolutely required.

On one hand, eval might be exploited by malicious users if they somehow manage to inject dynamic content. On the other hand, with the emergence of faster PHP runtimes like the HHVM, eval prevents some optimization that they perform.

Loading history...
66
            $packageModifiedFiles = array_keys($md5_string);
0 ignored issues
show
Bug introduced by
The variable $md5_string does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
67
68
            if ($filesToRemove = $zip->getFromName('filesToRemove.txt')) {
69
                $packageDeletedFiles = explode(PHP_EOL, str_replace(["\r\n", "\r", "\n"], PHP_EOL, $filesToRemove));
70
            } else if ($filesToRemove = $zip->getFromName('filesToRemove.json')) {
71
                $packageDeletedFiles = json_decode($filesToRemove);
72
            } else {
73
                throw new \RuntimeException('Can\'t open filesToRemove');
74
            }
75
76
            $modifiedFiles = array_merge($modifiedFiles, array_combine($packageModifiedFiles, array_fill(0, count($packageModifiedFiles), $package)));
77
            $deletedFiles = array_diff(array_merge($deletedFiles, $packageDeletedFiles), $packageModifiedFiles);
78
        }
79
80
        $modifiedFiles = array_keys(array_filter($modifiedFiles, function ($package, $changedFile) use ($buildPath, $packageZips) {
81
            if (($buildFile = @file_get_contents($buildPath . DS . $changedFile)) === false) {
82
                return false;
83
            }
84
            $packageFile = $packageZips[$package]->getFromName(basename($package, '.zip') . DS . $changedFile);
85
86
            return $this->getCheckSum($buildFile) != $this->getCheckSum($packageFile);
87
        }, ARRAY_FILTER_USE_BOTH));
88
89
        $deletedFiles = array_values(array_filter($deletedFiles));
90
91
        foreach ($packageZips as $zip) {
92
            $zip->close();
93
        }
94
95
        $modifiedFiles = array_values(array_filter($modifiedFiles, function ($file) use ($buildPath) {
96
            return file_exists($buildPath . '/custom/' . $file);
97
        }));
98
99
        $deletedFiles = array_values(array_filter($deletedFiles, function ($file) use ($buildPath) {
100
            return file_exists($buildPath . '/custom/' . $file);
101
        }));
102
103
        natsort($modifiedFiles);
104
        natsort($deletedFiles);
105
106
        return ['modified_files' => $modifiedFiles, 'deleted_files' => $deletedFiles];
107
    }
108
109
    /**
110
     * @param Upgrade $context
111
     *
112
     * @return array
113
     */
114
    private function getSuitablePackages(Upgrade $context)
115
    {
116
        if (!is_null($this->suitablePackages)) {
117
            return $this->suitablePackages;
118
        }
119
120
        $upgradeChains = $this->getUpgradeChains(
121
            $this->getFlavPackages($context->getBuildFlav(), $context->getTargetPath()),
122
            $context->getBuildVersion(),
123
            $context->getTargetVersion()
124
        );
125
126
        if (!$upgradeChains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $upgradeChains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
127
            $this->suitablePackages = [];
128
129
            return $this->suitablePackages;
130
        }
131
132
        usort($upgradeChains, function ($a1, $a2) {
133
            return count($a1) < count($a2) ? -1 : (count($a1) > count($a2) ? 1 : 0);
134
        });
135
136
        $this->suitablePackages = $upgradeChains[0];
137
138
        return $upgradeChains[0];
139
    }
140
141
    /**
142
     * Gets flav specific packages
143
     *
144
     * @param $flav
145
     * @param $packagesPath
146
     *
147
     * @return array
148
     */
149
    private function getFlavPackages($flav, $packagesPath)
150
    {
151
        $versionPattern = '/\d+\.\d+(\.\d+|\.x){1,2}/';
152
        $packagePattern = sprintf('/^Sugar%1$s-Upgrade-%2$s-to-%2$s.zip$/',
153
            ucfirst(mb_strtolower($flav)),
154
            trim($versionPattern, '/')
155
        );
156
157
        $packagesIterator = (new Finder())->files()->in($packagesPath)->name($packagePattern);
158
159
        $packages = [];
160
        foreach ($packagesIterator as $package) {
161
            if (preg_match_all($versionPattern, $package, $matches)) {
162
                $packages[$package->getFilename()] = [
163
                    'path' => $package->getRealPath(),
164
                    'from' => str_replace('.x', '', $matches[0][0]),
165
                    'to' => str_replace('.x', '', $matches[0][1])
166
                ];
167
            }
168
        }
169
170
        return $packages;
171
    }
172
173
    /**
174
     * @param $content
175
     *
176
     * @return string
177
     */
178
    private function getCheckSum($content)
179
    {
180
        // remove license comments
181
        $content = preg_replace('/\/\*.*?Copyright \(C\) SugarCRM.*?\*\//is', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
182
183
        // remove blank lines
184
        $content = preg_replace('/(^[\r\n]*|[\r\n]+)[\s\t]*[\r\n]+/', "\n", $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
185
186
        // change all line breaks to system line break
187
        $content = str_replace(["\r\n", "\r", "\n"], PHP_EOL, $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
188
189
        // remove trailing whitespaces
190
        $content = preg_replace('/^\s+|\s+$/m', '', $content);
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $content. This often makes code more readable.
Loading history...
191
192
        return md5($content);
193
    }
194
195
    /**
196
     * Calculates an array of possible upgrade chains
197
     *
198
     * @param $packages
199
     * @param $buildVersion
200
     * @param $upgradeTo
201
     *
202
     * @return array
203
     */
204
    private function getUpgradeChains($packages, $buildVersion, $upgradeTo)
205
    {
206
        // quit early
207
        if (!$packages) {
208
            return [];
209
        }
210
211
        $versionMatrix = $this->getVersionMatrix($packages);
212
        $allVersions = array_keys($versionMatrix);
213
214
        $getExistingSubversions = function ($version) use ($allVersions) {
0 ignored issues
show
Comprehensibility Naming introduced by
The variable name $getExistingSubversions exceeds the maximum configured length of 20.

Very long variable names usually make code harder to read. It is therefore recommended not to make variable names too verbose.

Loading history...
215
            $existingVersions = [];
216
217
            $fromParts = explode('.', $version);
218
            foreach (range(2, count($fromParts)) as $length) {
219
                $subversion = implode('.', array_slice($fromParts, 0, $length));
220
                if (in_array($subversion, $allVersions)) {
221
                    $existingVersions[] = $subversion;
222
                }
223
            }
224
225
            return $existingVersions;
226
        };
227
228
        // init chains with starting versions
229
        $chains = array_map(function ($version) use ($buildVersion) {
230
            return [$version => $buildVersion];
231
        }, $getExistingSubversions($buildVersion));
232
233
        // finish early if starting / ending version doesn't exist
234
        if (!$chains || !in_array($upgradeTo, $allVersions)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $chains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
235
            return [];
236
        }
237
238
        // gets last key of assoc array
239
        $getLastKey = function ($array) {
240
            end($array);
241
242
            return key($array);
243
        };
244
245
        // find all chains
246
        while (true) {
247
            $fullChains = [];
248
            foreach ($chains as $index => $chain) {
249
                $fromVersion = $getLastKey($chain);
250
251
                // skip not interesting chains
252
                if (version_compare($fromVersion, $upgradeTo, '>=')) {
253
                    continue;
254
                }
255
256
                $validChain = false;
257
                foreach ($allVersions as $version) {
258
                    if (!empty($versionMatrix[$fromVersion][$version])) {
259
                        $to = $getExistingSubversions($version);
260
                        foreach ($to as $toVersion) {
261
262
                            if ($toVersion === $fromVersion
263
                                || version_compare($toVersion, $upgradeTo, '>')
264
                                || $chain[$getLastKey($chain)] === $version
265
                            ) {
266
                                continue;
267
                            }
268
269
                            $validChain = true;
270
                            $fullChains[] = array_merge($chain, [$toVersion => $version]);
271
                        }
272
                    }
273
                }
274
275
                // remove invalid chain
276
                if (!$validChain) {
277
                    unset($chains[$index]);
278
                }
279
            }
280
281
            if (!$fullChains) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $fullChains of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
282
                break;
283
            }
284
285
            $chains = $fullChains;
286
        }
287
288
        $chains = array_map(function ($chain) use ($versionMatrix) {
289
            $keys = array_keys($chain);
290
            $values = array_values($chain);
291
292
            $packages = [];
293
            foreach (range(1, count($keys) - 1) as $index) {
294
                $packages[] = $versionMatrix[$keys[$index - 1]][$values[$index]];
295
            }
296
297
            return $packages;
298
        }, array_values($chains));
299
300
        return $chains;
301
    }
302
303
    /**
304
     * Creates version matrix
305
     *
306
     *       v1       v2       v3       v4
307
     *  v1    0     <path>   <path>      0
308
     *  v2    0        0     <path>      0
309
     *  v3    0        0        0     <path>
310
     *  v4    0        0        0        0
311
     *
312
     * @param $packages
313
     *
314
     * @return array
315
     */
316
    private function getVersionMatrix($packages)
317
    {
318
        $allVersions = array_unique(call_user_func_array('array_merge', array_map(function ($package) {
319
            return [$package['from'], $package['to']];
320
        }, $packages)));
321
322
        // sort versions (ASC)
323 View Code Duplication
        usort($allVersions, function ($v1, $v2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
324
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
325
        });
326
327
        // create matrix and fill it with zeros
328
        $versionMatrix = call_user_func_array('array_merge',array_map(function ($version) use ($allVersions) {
329
            return [$version => array_combine($allVersions, array_fill(0, count($allVersions), 0))];
330
        }, $allVersions));
331
332
        // valid associations point to package path
333
        foreach ($packages as $name => $package) {
334
            $versionMatrix[$package['from']][$package['to']] = $package['path'];
335
        }
336
337
        return $versionMatrix;
338
    }
339
}
340