Passed
Pull Request — master (#287)
by Théo
02:12
created

GenerateDockerFile::guessPharPath()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 42
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 42
rs 8.5706
c 0
b 0
f 0
cc 7
nc 7
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the box project.
7
 *
8
 * (c) Kevin Herrera <[email protected]>
9
 *     Théo Fidry <[email protected]>
10
 *
11
 * This source file is subject to the MIT license that is bundled
12
 * with this source code in the file LICENSE.
13
 */
14
15
namespace KevinGH\Box\Console\Command;
16
17
use Assert\Assertion;
18
use Composer\Semver\Semver;
19
use Symfony\Component\Console\Input\InputArgument;
20
use Symfony\Component\Console\Input\InputInterface;
21
use Symfony\Component\Console\Input\StringInput;
22
use Symfony\Component\Console\Output\OutputInterface;
23
use Symfony\Component\Console\Question\ConfirmationQuestion;
24
use Symfony\Component\Console\Style\SymfonyStyle;
25
use UnexpectedValueException;
26
use function array_column;
27
use function array_filter;
28
use function array_values;
29
use function basename;
30
use function file_exists;
31
use function getcwd;
32
use function implode;
33
use function KevinGH\Box\FileSystem\dump_file;
34
use function KevinGH\Box\FileSystem\make_path_relative;
35
use function KevinGH\Box\FileSystem\remove;
36
use function realpath;
37
use function sprintf;
38
use function str_replace;
39
40
/**
41
 * @private
42
 */
43
final class GenerateDockerFile extends ConfigurableCommand
44
{
45
    use CreateTemporaryPharFile;
46
47
    private const PHAR_ARG = 'phar';
48
49
    private const DOCKER_FILE_TEMPLATE = <<<'Dockerfile'
50
FROM php:__BASE_PHP_IMAGE_TOKEN__
51
52
RUN $(php -r '$extensionInstalled = array_map("strtolower", \get_loaded_extensions(false));$requiredExtensions = __PHP_EXTENSIONS_TOKEN__;$extensionsToInstall = array_diff($requiredExtensions, $extensionInstalled);if ([] !== $extensionsToInstall) {echo \sprintf("docker-php-ext-install %s", implode(" ", $extensionsToInstall));}echo "echo \"No extensions\"";')
53
54
COPY __PHAR_FILE_PATH_TOKEN__ /__PHAR_FILE_NAME_TOKEN__
55
56
ENTRYPOINT ["/__PHAR_FILE_NAME_TOKEN__"]
57
58
Dockerfile;
59
60
    private const PHP_DOCKER_IMAGES = [
61
        '7.2.0' => '7.2-cli-alpine',
62
        '7.1.0' => '7.1-cli-alpine',
63
        '7.0.0' => '7-cli-alpine',
64
    ];
65
66
    private const DOCKER_FILE_NAME = 'Dockerfile';
67
68
    /**
69
     * {@inheritdoc}
70
     */
71
    protected function configure(): void
72
    {
73
        parent::configure();
74
75
        $this->setName('docker');
76
        $this->setDescription('🐳  Generates a Dockerfile for the given PHAR');
77
        $this->addArgument(
78
            self::PHAR_ARG,
79
            InputArgument::OPTIONAL,
80
            'The PHAR file'
81
        );
82
    }
83
84
    /**
85
     * {@inheritdoc}
86
     */
87
    protected function execute(InputInterface $input, OutputInterface $output): int
88
    {
89
        $io = new SymfonyStyle($input, $output);
90
91
        $pharPath = $input->getArgument(self::PHAR_ARG);
92
93
        if (null === $pharPath) {
94
            $pharPath = $this->guessPharPath($input, $output, $io);
95
        }
96
97
        if (null === $pharPath) {
98
            return 1;
99
        }
100
101
        Assertion::file($pharPath);
0 ignored issues
show
Bug introduced by
It seems like $pharPath can also be of type string[]; however, parameter $value of Assert\Assertion::file() does only seem to accept string, 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

101
        Assertion::file(/** @scrutinizer ignore-type */ $pharPath);
Loading history...
102
103
        $pharPath = false !== realpath($pharPath) ? realpath($pharPath) : $pharPath;
104
105
        $io->newLine();
106
        $io->writeln(
107
            sprintf(
108
                '🐳  Generating a Dockerfile for the PHAR "<comment>%s</comment>"',
109
                $pharPath
110
            )
111
        );
112
113
        $tmpPharPath = $this->createTemporaryPhar($pharPath);
114
115
        $requirementsPhar = 'phar://'.$tmpPharPath.'/.box/.requirements.php';
116
117
        $dockerFileContents = self::DOCKER_FILE_TEMPLATE;
118
119
        try {
120
            if (false === file_exists($requirementsPhar)) {
121
                $io->error(
122
                    'Cannot retrieve the requirements for the PHAR. Make sure the PHAR has been built with Box and the '
123
                    .'requirement checker enabled.'
124
                );
125
            }
126
127
            $requirements = include $requirementsPhar;
128
129
            $dockerFileContents = str_replace(
130
                '__BASE_PHP_IMAGE_TOKEN__',
131
                $this->retrievePhpImageName($requirements),
132
                $dockerFileContents
133
            );
134
135
            $dockerFileContents = str_replace(
136
                '__PHP_EXTENSIONS_TOKEN__',
137
                $this->retrievePhpExtensions($requirements),
138
                $dockerFileContents
139
            );
140
141
            $dockerFileContents = str_replace(
142
                '__PHAR_FILE_PATH_TOKEN__',
143
                make_path_relative($pharPath, getcwd()),
144
                $dockerFileContents
145
            );
146
147
            $dockerFileContents = str_replace(
148
                '__PHAR_FILE_NAME_TOKEN__',
149
                basename($pharPath),
150
                $dockerFileContents
151
            );
152
153
            dump_file(self::DOCKER_FILE_NAME, $dockerFileContents);
154
155
            if (file_exists(self::DOCKER_FILE_NAME)) {
156
                $remove = $io->askQuestion(
157
                    new ConfirmationQuestion(
158
                        'A Docker file has already been found, are you sure you want to override it?',
159
                        true
160
                    )
161
                );
162
163
                if (false === $remove) {
164
                    $io->writeln('Skipped the docker file generation.');
165
166
                    return 0;
167
                }
168
            }
169
170
            $io->success('Done');
171
172
            $io->writeln(
173
                [
174
                    sprintf(
175
                        'You can now inspect your <comment>%s</comment> file or build your container with:',
176
                        self::DOCKER_FILE_NAME
177
                    ),
178
                    '$ <comment>docker build .</comment>',
179
                ]
180
            );
181
        } finally {
182
            if ($tmpPharPath !== $pharPath) {
183
                remove($tmpPharPath);
184
            }
185
        }
186
187
        return 0;
188
    }
189
190
    private function retrievePhpImageName(array $requirements): string
191
    {
192
        $conditions = array_column(
193
            array_filter(
194
                $requirements,
195
                function (array $requirement): bool {
196
                    return 'php' === $requirement['type'];
197
                }
198
            ),
199
            'condition'
200
        );
201
202
        foreach (self::PHP_DOCKER_IMAGES as $php => $image) {
203
            foreach ($conditions as $condition) {
204
                if (false === Semver::satisfies($php, $condition)) {
205
                    continue 2;
206
                }
207
            }
208
209
            return $image;
210
        }
211
212
        throw new UnexpectedValueException(
213
            sprintf(
214
                'Could not find a suitable Docker base image for the PHP constraint(s) "%s". Images available: "%s"',
215
                implode('", "', $conditions),
216
                implode('", "', array_values(self::PHP_DOCKER_IMAGES))
217
            )
218
        );
219
    }
220
221
    private function retrievePhpExtensions(array $requirements): string
222
    {
223
        $extensions = array_column(
224
            array_filter(
225
                $requirements,
226
                function (array $requirement): bool {
227
                    return 'extension' === $requirement['type'];
228
                }
229
            ),
230
            'condition'
231
        );
232
233
        if ([] === $extensions) {
234
            return '[]';
235
        }
236
237
        return sprintf(
238
            '["%s"]',
239
            implode(
240
                '", "',
241
                $extensions
242
            )
243
        );
244
    }
245
246
    private function guessPharPath(InputInterface $input, OutputInterface $output, SymfonyStyle $io): ?string
247
    {
248
        $config = $this->getConfig($input, $output, true);
249
250
        if (file_exists($config->getOutputPath())) {
251
            return $config->getOutputPath();
252
        }
253
254
        $compile = $io->askQuestion(
255
            new ConfirmationQuestion(
256
                'The output PHAR could not be found, do you wish to generate it by running "<comment>box '
257
                .'compile</comment>"?',
258
                true
259
            )
260
        );
261
262
        if (false === $compile) {
263
            $io->error('Could not find the PHAR to generate the docker file for');
264
265
            return null;
266
        }
267
268
        $compileCommand = $this->getApplication()->find('compile');
269
270
        if ($output->isQuiet()) {
271
            $compileInput = '--quiet';
272
        } elseif ($output->isVerbose()) {
273
            $compileInput = '--verbose 1';
274
        } elseif ($output->isVeryVerbose()) {
275
            $compileInput = '--verbose 2';
276
        } elseif ($output->isDebug()) {
277
            $compileInput = '--verbose 3';
278
        } else {
279
            $compileInput = '';
280
        }
281
282
        $compileInput = new StringInput($compileInput);
283
        $compileInput->setInteractive(false);
284
285
        $compileCommand->run($compileInput, $output);
286
287
        return $config->getOutputPath();
288
    }
289
}
290