Passed
Push — 6.5.0.0 ( 85bd4e...fe6596 )
by Christian
24:46 queued 09:10
created

UpdateController::updateComposerJsonConstraint()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 35
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 19
c 2
b 0
f 0
nc 5
nop 2
dl 0
loc 35
rs 9.3222
1
<?php
2
declare(strict_types=1);
3
4
namespace App\Controller;
5
6
use App\Services\FlexMigrator;
7
use App\Services\RecoveryManager;
8
use App\Services\ReleaseInfoProvider;
9
use App\Services\StreamedCommandResponseGenerator;
10
use Composer\Util\Platform;
11
use Shopware\Core\Framework\Log\Package;
12
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
13
use Symfony\Component\HttpFoundation\Request;
14
use Symfony\Component\HttpFoundation\Response;
15
use Symfony\Component\Routing\Annotation\Route;
16
17
/**
18
 * @internal
19
 */
20
#[Package('core')]
21
class UpdateController extends AbstractController
22
{
23
    public function __construct(
24
        private readonly RecoveryManager $recoveryManager,
25
        private readonly ReleaseInfoProvider $releaseInfoProvider,
26
        private readonly FlexMigrator $flexMigrator,
27
        private readonly StreamedCommandResponseGenerator $streamedCommandResponseGenerator
28
    ) {
29
    }
30
31
    #[Route('/update', name: 'update', defaults: ['step' => 2], methods: ['GET'])]
32
    public function index(Request $request): Response
33
    {
34
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
35
36
        $currentShopwareVersion = $this->recoveryManager->getCurrentShopwareVersion($shopwarePath);
37
        $latestVersion = $this->getLatestVersion($request);
38
39
        if ($currentShopwareVersion === $latestVersion) {
40
            return $this->redirectToRoute('finish');
41
        }
42
43
        return $this->render('update.html.twig', [
44
            'shopwarePath' => $shopwarePath,
45
            'currentShopwareVersion' => $currentShopwareVersion,
46
            'isFlexProject' => $this->recoveryManager->isFlexProject($shopwarePath),
47
            'latestShopwareVersion' => $latestVersion,
48
        ]);
49
    }
50
51
    #[Route('/update/_migrate-template', name: 'migrate-template', methods: ['POST'])]
52
    public function migrateTemplate(): Response
53
    {
54
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
55
56
        $this->flexMigrator->cleanup($shopwarePath);
57
        $this->flexMigrator->patchRootComposerJson($shopwarePath);
58
        $this->flexMigrator->copyNewTemplateFiles($shopwarePath);
59
        $this->flexMigrator->migrateEnvFile($shopwarePath);
60
61
        return new Response('', Response::HTTP_NO_CONTENT);
62
    }
63
64
    #[Route('/update/_run', name: 'update_run', methods: ['POST'])]
65
    public function run(Request $request): Response
66
    {
67
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
68
69
        $this->updateComposerJsonConstraint($request, $shopwarePath . '/composer.json');
70
71
        return $this->streamedCommandResponseGenerator->runJSON([
72
            $this->recoveryManager->getPhpBinary($request),
73
            $this->recoveryManager->getBinary(),
74
            'composer',
75
            'update',
76
            '-d',
77
            $shopwarePath,
78
            '--no-interaction',
79
            '--no-ansi',
80
            '--no-scripts',
81
            '-v',
82
            '--with-all-dependencies', // update all packages
83
        ]);
84
    }
85
86
    #[Route('/update/_reset_config', name: 'update_reset_config', methods: ['POST'])]
87
    public function resetConfig(Request $request): Response
88
    {
89
        if (\function_exists('opcache_reset')) {
90
            opcache_reset();
91
        }
92
93
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
94
95
        $this->patchSymfonyFlex($shopwarePath);
96
97
        return $this->streamedCommandResponseGenerator->runJSON([
98
            $this->recoveryManager->getPhpBinary($request),
99
            $this->recoveryManager->getBinary(),
100
            'composer',
101
            '-d',
102
            $shopwarePath,
103
            'symfony:recipes:install',
104
            '--force',
105
            '--reset',
106
            '--no-interaction',
107
            '--no-ansi',
108
            '-v',
109
        ]);
110
    }
111
112
    #[Route('/update/_prepare', name: 'update_prepare', methods: ['POST'])]
113
    public function prepare(Request $request): Response
114
    {
115
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
116
117
        return $this->streamedCommandResponseGenerator->runJSON([
118
            $this->recoveryManager->getPhpBinary($request),
119
            $shopwarePath . '/bin/console',
120
            'system:update:prepare',
121
            '--no-interaction',
122
        ]);
123
    }
124
125
    #[Route('/update/_finish', name: 'update_finish', methods: ['POST'])]
126
    public function finish(Request $request): Response
127
    {
128
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
129
130
        return $this->streamedCommandResponseGenerator->runJSON([
131
            $this->recoveryManager->getPhpBinary($request),
132
            $shopwarePath . '/bin/console',
133
            'system:update:finish',
134
            '--no-interaction',
135
        ]);
136
    }
137
138
    /**
139
     * @see https://github.com/symfony/flex/pull/963
140
     */
141
    public function patchSymfonyFlex(string $shopwarePath): void
142
    {
143
        $optionsPhp = (string) file_get_contents($shopwarePath . '/vendor/symfony/flex/src/Options.php');
144
145
        $optionsPhp = str_replace(
146
            'return $this->io && $this->io->askConfirmation(sprintf(\'Cannot determine the state of the "%s" file, overwrite anyway? [y/N] \', $file), false);',
147
            'return $this->io && $this->io->askConfirmation(sprintf(\'Cannot determine the state of the "%s" file, overwrite anyway? [y/N] \', $file));',
148
            $optionsPhp
149
        );
150
151
        $optionsPhp = str_replace(
152
            'return $this->io && $this->io->askConfirmation(sprintf(\'File "%s" has uncommitted changes, overwrite? [y/N] \', $name), false);',
153
            'return $this->io && $this->io->askConfirmation(sprintf(\'File "%s" has uncommitted changes, overwrite? [y/N] \', $name));',
154
            $optionsPhp
155
        );
156
157
        file_put_contents($shopwarePath . '/vendor/symfony/flex/src/Options.php', $optionsPhp);
158
    }
159
160
    private function getLatestVersion(Request $request): string
161
    {
162
        if ($request->getSession()->has('latestVersion')) {
163
            $sessionValue = $request->getSession()->get('latestVersion');
164
            \assert(\is_string($sessionValue));
165
166
            return $sessionValue;
167
        }
168
169
        $latestVersions = $this->releaseInfoProvider->fetchLatestRelease();
170
171
        $shopwarePath = $this->recoveryManager->getShopwareLocation();
172
        \assert(\is_string($shopwarePath));
173
174
        $currentVersion = $this->recoveryManager->getCurrentShopwareVersion($shopwarePath);
175
        $latestVersion = $latestVersions[substr($currentVersion, 0, 3)];
176
177
        // If the user is already on the latest version in the current major, we need to update to the next major
178
        if ($latestVersion === $currentVersion) {
179
            $first = (int) substr($currentVersion, 0, 1);
180
            $second = (int) substr($currentVersion, 2, 1);
181
            ++$second;
182
183
            if (isset($latestVersions[$first . '.' . $second])) {
184
                $latestVersion = $latestVersions[$first . '.' . $second];
185
            }
186
        }
187
188
        $request->getSession()->set('latestVersion', $latestVersion);
189
190
        return $latestVersion;
191
    }
192
193
    private function updateComposerJsonConstraint(Request $request, string $file): void
194
    {
195
        $shopwarePackages = [
196
            'shopware/core',
197
            'shopware/administration',
198
            'shopware/storefront',
199
            'shopware/elasticsearch',
200
        ];
201
202
        /** @var array{require: array<string, string>} $composerJson */
203
        $composerJson = json_decode((string) file_get_contents($file), true, \JSON_THROW_ON_ERROR);
204
        $latestVersion = $this->getLatestVersion($request);
205
206
        foreach ($shopwarePackages as $shopwarePackage) {
207
            if (!isset($composerJson['require'][$shopwarePackage])) {
208
                continue;
209
            }
210
211
            // Lock the composer version to that major version
212
            $version = '~' . substr($latestVersion, 0, 3) . '.0';
213
214
            $nextVersion = Platform::getEnv('SW_RECOVERY_NEXT_VERSION');
215
            if (\is_string($nextVersion)) {
216
                $nextBranch = Platform::getEnv('SW_RECOVERY_NEXT_BRANCH');
217
                if ($nextBranch === false) {
218
                    $nextBranch = 'dev-trunk';
219
                }
220
221
                $version = $nextBranch . ' as ' . $nextVersion;
222
            }
223
224
            $composerJson['require'][$shopwarePackage] = $version;
225
        }
226
227
        file_put_contents($file, json_encode($composerJson, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
228
    }
229
}
230