Completed
Push — master ( 9226b3...e0bc2b )
by Mike
08:41
created

UpgradeChanges::getFlavPackages()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 14
c 0
b 0
f 0
nc 3
nop 2
dl 0
loc 23
rs 9.0856
1
<?php
2
3
namespace Sugarcrm\UpgradeSpec\Element\Section;
4
5
use Sugarcrm\UpgradeSpec\Data\DataAwareInterface;
6
use Sugarcrm\UpgradeSpec\Data\DataAwareTrait;
7
use Sugarcrm\UpgradeSpec\Element\ElementInterface;
8
use Sugarcrm\UpgradeSpec\Spec\Context;
9
use Sugarcrm\UpgradeSpec\Template\RendererAwareInterface;
10
use Sugarcrm\UpgradeSpec\Template\RendererAwareTrait;
11
use Symfony\Component\Finder\Finder;
12
13
class UpgradeChanges implements ElementInterface, RendererAwareInterface, DataAwareInterface
14
{
15
    use RendererAwareTrait, DataAwareTrait;
16
17
    /**
18
     * @return string
19
     */
20
    public function getTitle()
21
    {
22
        return 'Review upgrade changes and fix possible customization conflicts';
23
    }
24
25
    /**
26
     * @return int
27
     */
28
    public function getOrder()
29
    {
30
        return 3;
31
    }
32
33
    /**
34
     * @param Context $context
35
     *
36
     * @return bool
37
     */
38
    public function isRelevantTo(Context $context)
39
    {
40
        return $context->getPackagesPath()
41
            && $this->getFlavPackages($context->getFlav(), $context->getPackagesPath());
42
    }
43
44
    /**
45
     * @param Context $context
46
     *
47
     * @return string
48
     */
49
    public function getBody(Context $context)
50
    {
51
        return $this->renderer->render('upgrade_changes', [
52
            'packages' => $this->getSuitablePackages($context),
53
            'upgrade_to' => $context->getUpgradeVersion(),
54
            'packages_path' => $context->getPackagesPath(),
55
        ]);
56
    }
57
58
    /**
59
     * @param Context $context
60
     *
61
     * @return array
62
     */
63
    private function getSuitablePackages(Context $context)
64
    {
65
        $chains = $this->getUpgradeChains(
66
            $this->getFlavPackages($context->getFlav(), $context->getPackagesPath()),
67
            $context->getBuildVersion(),
68
            $context->getUpgradeVersion()
69
        );
70
71
        if (!$chains) {
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...
72
            return [];
73
        }
74
75
        usort($chains, function ($a1, $a2) {
76
            return count($a1) < count($a2) ? -1 : (count($a1) > count($a2) ? 1 : 0);
77
        });
78
79
        return $chains[0];
80
    }
81
82
    /**
83
     * Gets flav specific packages
84
     *
85
     * @param $flav
86
     * @param $packagesPath
87
     *
88
     * @return array
89
     */
90
    private function getFlavPackages($flav, $packagesPath)
91
    {
92
        $versionPattern = '/\d+\.\d+(\.\d+|\.x){1,2}/';
93
        $packagePattern = sprintf('/^Sugar%1$s-Upgrade-%2$s-to-%2$s.zip$/',
94
            ucfirst(mb_strtolower($flav)),
95
            trim($versionPattern, '/')
96
        );
97
98
        $packagesIterator = (new Finder())->files()->in($packagesPath)->name($packagePattern);
99
100
        $packages = [];
101
        foreach ($packagesIterator as $package) {
102
            if (preg_match_all($versionPattern, $package, $matches)) {
103
                $packages[$package->getFilename()] = [
104
                    'path' => $package->getRealPath(),
105
                    'from' => str_replace('.x', '', $matches[0][0]),
106
                    'to' => str_replace('.x', '', $matches[0][1])
107
                ];
108
            }
109
        }
110
111
        return $packages;
112
    }
113
114
    /**
115
     * Calculates an array of possible upgrade chains
116
     *
117
     * @param $packages
118
     * @param $buildVersion
119
     * @param $upgradeTo
120
     *
121
     * @return array
122
     */
123
    private function getUpgradeChains($packages, $buildVersion, $upgradeTo)
124
    {
125
        $versionMatrix = $this->getVersionMatrix($packages);
126
        $allVersions = array_keys($versionMatrix);
127
128
        $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...
129
            $existingVersions = [];
130
131
            $fromParts = explode('.', $version);
132
            foreach (range(2, count($fromParts)) as $length) {
133
                $subversion = implode('.', array_slice($fromParts, 0, $length));
134
                if (in_array($subversion, $allVersions)) {
135
                    $existingVersions[] = $subversion;
136
                }
137
            }
138
139
            return $existingVersions;
140
        };
141
142
        // init chains with starting versions
143
        $chains = array_map(function ($version) use ($buildVersion) {
144
            return [$version => $buildVersion];
145
        }, $getExistingSubversions($buildVersion));
146
147
        // finish early if starting / ending version doesn't exist
148
        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...
149
            return [];
150
        }
151
152
        // gets last key of assoc array
153
        $getLastKey = function ($array) {
154
            end($array);
155
156
            return key($array);
157
        };
158
159
        // find all chains
160
        while (true) {
161
            $fullChains = [];
162
            foreach ($chains as $index => $chain) {
163
                $fromVersion = $getLastKey($chain);
164
165
                // skip not interesting chains
166
                if (version_compare($fromVersion, $upgradeTo, '>=')) {
167
                    continue;
168
                }
169
170
                $validChain = false;
171
                foreach ($allVersions as $version) {
172
                    if (!empty($versionMatrix[$fromVersion][$version])) {
173
                        $to = $getExistingSubversions($version);
174
                        foreach ($to as $toVersion) {
175
176
                            if ($toVersion === $fromVersion
177
                                || version_compare($toVersion, $upgradeTo, '>')
178
                                || $chain[$getLastKey($chain)] === $version
179
                            ) {
180
                                continue;
181
                            }
182
183
                            $validChain = true;
184
                            $fullChains[] = array_merge($chain, [$toVersion => $version]);
185
                        }
186
                    }
187
                }
188
189
                // remove invalid chain
190
                if (!$validChain) {
191
                    unset($chains[$index]);
192
                }
193
            }
194
195
            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...
196
                break;
197
            }
198
199
            $chains = $fullChains;
200
        }
201
202
        $chains = array_map(function ($chain) use ($versionMatrix) {
203
            $keys = array_keys($chain);
204
            $values = array_values($chain);
205
206
            $packages = [];
207
            foreach (range(1, count($keys) - 1) as $index) {
208
                $packages[] = $versionMatrix[$keys[$index - 1]][$values[$index]];
209
            }
210
211
            return $packages;
212
        }, array_values($chains));
213
214
        return $chains;
215
    }
216
217
    /**
218
     * Creates version matrix
219
     *
220
     *       v1       v2       v3       v4
221
     *  v1    0     <path>   <path>      0
222
     *  v2    0        0     <path>      0
223
     *  v3    0        0        0     <path>
224
     *  v4    0        0        0        0
225
     *
226
     * @param $packages
227
     *
228
     * @return array
229
     */
230
    private function getVersionMatrix($packages)
231
    {
232
        $allVersions = array_unique(call_user_func_array('array_merge', array_map(function ($package) {
233
            return [$package['from'], $package['to']];
234
        }, $packages)));
235
236
        // sort versions (ASC)
237 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...
238
            return version_compare($v1, $v2, '<') ? -1 : (version_compare($v1, $v2, '>') ? 1 : 0);
239
        });
240
241
        // create matrix and fill it with zeros
242
        $versionMatrix = call_user_func_array('array_merge',array_map(function ($version) use ($allVersions) {
243
            return [$version => array_combine($allVersions, array_fill(0, count($allVersions), 0))];
244
        }, $allVersions));
245
246
        // valid associations point to package path
247
        foreach ($packages as $name => $package) {
248
            $versionMatrix[$package['from']][$package['to']] = $package['path'];
249
        }
250
251
        return $versionMatrix;
252
    }
253
}
254