Passed
Pull Request — master (#9)
by Julien
04:54
created

StartEventCommand   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 316
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 209
dl 0
loc 316
rs 9.68
c 0
b 0
f 0
wmc 34

3 Methods

Rating   Name   Duplication   Size   Complexity  
F executeEvent() 0 268 28
A getEventName() 0 3 1
A getAvailableVersionParts() 0 36 5
1
<?php
2
3
namespace TheAentMachine\AentPhp\Command;
4
5
use Symfony\Component\Console\Question\Question;
6
use TheAentMachine\CommonEvents;
7
use TheAentMachine\Command\EventCommand;
8
use TheAentMachine\Aenthill\Pheromone;
9
use TheAentMachine\Registry\RegistryClient;
10
use TheAentMachine\Service\Service;
11
12
class StartEventCommand extends EventCommand
13
{
14
    protected function getEventName(): string
15
    {
16
        return 'START';
17
    }
18
19
    protected function executeEvent(?string $payload): ?string
20
    {
21
        $commentEvents = new CommonEvents($this->getAentHelper(), $this->output);
22
23
        /************************ Environments **********************/
24
        $environments = $this->getAentHelper()->askForEnvironments();
25
        if (empty($environments)) {
26
            $this->output->writeln("<error>No environments available, did you forget to install an aent like theaentmachine/aent-docker-compose?</error>");
27
            return null;
28
        }
29
30
        $service = new Service();
31
32
        /************************ Service name **********************/
33
        $serviceName = $this->getAentHelper()->askForServiceName('app', 'Your PHP application');
34
        $this->output->writeln("<info>You are about to create a '$serviceName' PHP container</info>");
35
        $service->setServiceName($serviceName);
36
37
        /************************ PHP Version **********************/
38
        [
39
            'phpVersions' => $phpVersions,
40
            'variants' => $variants,
41
            'nodeVersions' => $nodeVersions,
42
        ] = $this->getAvailableVersionParts();
43
44
        $phpVersion = $this->getAentHelper()
45
            ->choiceQuestion(
46
                'PHP version',
47
                $phpVersions
48
            )
49
            ->setDefault('0')
50
            ->ask();
51
        $this->output->writeln("<info>You are about to install PHP $phpVersion</info>");
52
        $this->getAentHelper()->spacer();
53
54
55
        $variant = $this->getAentHelper()
56
            ->choiceQuestion(
57
                'Variant',
58
                $variants
59
            )
60
            ->setDefault('0')
61
            ->ask();
62
        $this->output->writeln("<info>You selected the $variant variant</info>");
63
        $this->getAentHelper()->spacer();
64
65
        $node = $this->getAentHelper()
66
            ->question('Do you want to install NodeJS?')
67
            ->yesNoQuestion()
68
            ->compulsory()
69
            ->ask();
70
71
        if ($node) {
72
            $this->output->writeln("<info>The image will also contain NodeJS</info>");
73
            $node = $this->getAentHelper()
74
                ->choiceQuestion(
75
                    'NodeJS version',
76
                    $nodeVersions
77
                )
78
                ->setDefault('0')
79
                ->ask();
80
            $this->output->writeln("<info>You selected the version $node</info>");
81
            $node = '-' . $node;
82
            $this->getAentHelper()->spacer();
83
        } else {
84
            $this->output->writeln('<info>The image will not contain NodeJS</info>');
85
        }
86
87
        $service->setImage("thecodingmachine/php:$phpVersion-v1-$variant$node");
88
89
        /************************ Root application path **********************/
90
        $this->output->writeln('Now, we need to find the root of your web application.');
91
        $appDirectory = $this->getAentHelper()->question('PHP application root directory (relative to the project root directory)')
92
            ->setHelpText('Your PHP application root directory is typically the directory that contains your composer.json file. It must be relative to the project root directory.')
93
            ->setValidator(function (string $appDirectory) {
94
                $appDirectory = trim($appDirectory, '/') ?: '.';
95
                $rootDir = Pheromone::getContainerProjectDirectory();
96
97
                $fullDir = $rootDir.'/'.$appDirectory;
98
                if (!is_dir($fullDir)) {
99
                    throw new \InvalidArgumentException('Could not find directory '.Pheromone::getHostProjectDirectory().'/'.$appDirectory);
100
                }
101
                return $appDirectory;
102
            })->ask();
103
104
        $this->output->writeln('<info>Your root PHP application directory is '.Pheromone::getHostProjectDirectory().'/'.$appDirectory.'</info>');
105
        $this->getAentHelper()->spacer();
106
107
        $service->addBindVolume('./'.$appDirectory, '/var/www/html');
108
109
        /************************ Web application path **********************/
110
        if ($variant === 'apache') {
111
            $answer = $this->getAentHelper()->question('Do you have a public web folder that is not the root of your application?')
112
                ->yesNoQuestion()->setDefault('y')->ask();
113
            if ($answer) {
114
                $webDirectory = $this->getAentHelper()->question('Web directory (relative to the PHP application directory)')
115
                    ->setHelpText('Your PHP application web directory is typically the directory that contains your index.php file. It must be relative to the PHP application directory ('.Pheromone::getHostProjectDirectory().'/'.$appDirectory.')')
116
                    ->setValidator(function (string $webDirectory) use ($appDirectory) {
117
                        $webDirectory = trim($webDirectory, '/') ?: '.';
118
                        $rootDir = Pheromone::getContainerProjectDirectory();
119
120
                        $fullDir = $rootDir.'/'.$appDirectory.'/'.$webDirectory;
121
                        if (!is_dir($fullDir)) {
122
                            throw new \InvalidArgumentException('Could not find directory '.Pheromone::getHostProjectDirectory().'/'.$appDirectory.'/'.$webDirectory);
123
                        }
124
                        return $webDirectory;
125
                    })->ask();
126
127
                $service->addImageEnvVariable('APACHE_DOCUMENT_ROOT', $webDirectory);
128
                $this->output->writeln('<info>Your web directory is '.Pheromone::getHostProjectDirectory().'/'.$appDirectory.'/'.$webDirectory.'</info>');
129
                $this->getAentHelper()->spacer();
130
            }
131
        }
132
133
        /************************ Upload path **********************/
134
        $this->output->writeln('Now, we need to know if there are directories you want to store <info>out of the container</info>.');
135
        $this->output->writeln('When a container is removed, anything in it is lost. If your application is letting users upload files, or if it generates files, it might be important to <comment>store those files out of the container</comment>.');
136
        $this->output->writeln('If you want to mount such a directory out of the container, please specify the directory path below. Path must be relative to the PHP application root directory.');
137
        $this->getAentHelper()->spacer();
138
139
        $uploadDirs = [];
140
        do {
141
            $uploadDirectory = $this->getAentHelper()
142
                ->question('Please input directory (for instance for file uploads) that you want to mount out of the container? (keep empty to ignore)')
143
                ->setDefault('')
144
                ->ask();
145
146
            $uploadDirectory = trim($uploadDirectory, '/');
147
            $rootDir = Pheromone::getContainerProjectDirectory();
148
149
            if ($uploadDirectory !== '') {
150
                $fullDir = $rootDir.'/'.$appDirectory.'/'.$uploadDirectory;
151
                if (!is_dir($fullDir)) {
152
                    $this->output->writeln('<error>Could not find directory '.Pheromone::getHostProjectDirectory().'/'.$appDirectory.'/'.$uploadDirectory.'</error>');
153
                    $uploadDirectory = null;
154
                } else {
155
                    $uploadDirs[] = $uploadDirectory;
156
                    $this->output->writeln('<info>Directory '.Pheromone::getHostProjectDirectory().'/'.$appDirectory.'/'.$uploadDirectory.' will be stored out of the container</info>');
157
158
                    $volumeName = $this->getAentHelper()
159
                        ->question('Please input directory (for instance for file uploads) that you want to mount out of the container? (keep empty to ignore)')
160
                        ->setDefault('')
161
                        ->compulsory()
162
                        ->setValidator(function (string $value) {
163
                            $value = trim($value);
164
                            if (!\preg_match('/^[a-zA-Z0-9_.-]+$/', $value)) {
165
                                throw new \InvalidArgumentException('Invalid volume name "' . $value . '". Volume names can contain alphanumeric characters, and "_", ".", "-".');
166
                            }
167
168
                            return $value;
169
                        })
170
                        ->ask();
171
                    $question = new Question('What name should we use for this volume? ', '');
172
                    $question->setValidator(function (string $value) {
173
                        $value = trim($value);
174
                        if (!\preg_match('/^[a-zA-Z0-9_.-]+$/', $value)) {
175
                            throw new \InvalidArgumentException('Invalid volume name "'.$value.'". Volume names can contain alphanumeric characters, and "_", ".", "-".');
176
                        }
177
178
                        return $value;
179
                    });
180
181
                    $service->addNamedVolume($volumeName, $appDirectory.'/'.$uploadDirectory);
182
                }
183
            }
184
        } while ($uploadDirectory !== '');
185
        $this->getAentHelper()->spacer();
186
187
        $availableExtensions = ['amqp', 'ast', 'bcmath', 'bz2', 'calendar', 'dba', 'enchant', 'ev', 'event', 'exif',
188
            'gd', 'gettext', 'gmp', 'igbinary', 'imap', 'intl', 'ldap', 'mcrypt', 'memcached', 'mongodb', 'pcntl',
189
            'pdo_dblib', 'pdo_pgsql', 'pgsql', 'pspell', 'shmop', 'snmp', 'sockets', 'sysvmsg', 'sysvsem', 'sysvshm',
190
            'tidy', 'wddx', 'weakref', 'xdebug', 'xmlrpc', 'xsl', 'yaml'];
191
192
        $this->output->writeln('By default, the following extensions are enabled:');
193
        $this->output->writeln('<info>apcu mysqli opcache pdo pdo_mysql redis zip soap mbstring ftp mysqlnd</info>');
194
        $this->output->writeln('You can select more extensions below:');
195
        $this->output->writeln('<info>'.\implode(' ', $availableExtensions).'</info>');
196
197
        /************************ Extensions **********************/
198
        $extensions = [];
199
        do {
200
            $question = new Question('Please enter the name of an additional extension you want to install (keep empty to skip): ', '');
201
            $question->setAutocompleterValues($availableExtensions);
202
            $question->setValidator(function (string $value) use ($availableExtensions) {
203
                if (trim($value) !== '' && !\in_array($value, $availableExtensions)) {
204
                    throw new \InvalidArgumentException('Unknown extension '.$value);
205
                }
206
207
                return trim($value);
208
            });
209
210
            $extension = $this->getHelper('question')->ask($this->input, $this->output, $question);
211
212
            if ($extension !== '') {
213
                $service->addImageEnvVariable('PHP_EXTENSION_'.\strtoupper($extension), '1');
214
                $extensions[] = $extension;
215
            }
216
        } while ($extension !== '');
217
        $this->output->writeln('<info>Enabled extensions: apcu mysqli opcache pdo pdo_mysql redis zip soap mbstring ftp mysqlnd '.\implode(' ', $extensions).'</info>');
218
        $this->getAentHelper()->spacer();
219
220
221
        /************************ php.ini settings **********************/
222
        $this->output->writeln("Now, let's customize some settings of <info>php.ini</info>.");
223
224
        $memoryLimit = $this->getAentHelper()->question('PHP <info>memory limit</info> (keep empty to stay with the default 128M)')
225
            ->setHelpText('This value will be used in the memory_limit option of PHP via the PHP_INI_MEMORY_LIMIT environment variable.')
226
            ->setValidator(function (string $value) {
227
                if (trim($value) !== '' && !\preg_match('/^[0-9]+([MGK])?$/i', $value)) {
228
                    throw new \InvalidArgumentException('Invalid value: '.$value);
229
                }
230
231
                return trim($value);
232
            })
233
            ->ask();
234
        if ($memoryLimit !== '') {
235
            $this->output->writeln("<info>Memory limit: $memoryLimit</info>");
236
            $service->addImageEnvVariable('PHP_INI_MEMORY_LIMIT', $memoryLimit);
237
        }
238
239
        $uploadMaxFileSize = $this->getAentHelper()->question('<info>Maximum file size for uploaded files</info> (keep empty to stay with the default 2M)')
240
            ->setHelpText('This value will be used in the upload_max_file_size and post_max_size options of PHP via the PHP_INI_UPLOAD_MAX_FILESIZE and PHP_INI_POST_MAX_SIZE environment variables.')
241
            ->setValidator(function (string $value) {
242
                if (trim($value) !== '' && !\preg_match('/^[0-9]+([MGK])?$/i', $value)) {
243
                    throw new \InvalidArgumentException('Invalid value: '.$value);
244
                }
245
246
                return trim($value);
247
            })
248
            ->ask();
249
250
        if ($uploadMaxFileSize !== '') {
251
            $this->output->writeln("<info>Upload maximum file size: $uploadMaxFileSize</info>");
252
            $service->addImageEnvVariable('PHP_INI_UPLOAD_MAX_FILESIZE', $uploadMaxFileSize);
253
            $service->addImageEnvVariable('PHP_INI_POST_MAX_SIZE', $uploadMaxFileSize);
254
        }
255
        $this->getAentHelper()->spacer();
256
257
        $this->output->writeln('Does your service depends on another service to start? For instance a "mysql" instance?');
258
        do {
259
            $depend = $this->getAentHelper()
260
                ->question('Please input a service name your application depends on (keep empty to skip)')
261
                ->setDefault('')
262
                ->ask();
263
264
            if ($depend !== '') {
265
                $service->addDependsOn($depend);
266
                $this->output->writeln('<info>Added dependency: '.$depend.'</info>');
267
            }
268
        } while ($depend !== '');
269
        $this->getAentHelper()->spacer();
270
271
272
        // TODO: propose to run composer install on startup?
273
274
        if ($variant === 'apache') {
275
            $service->addInternalPort(80);
276
        }
277
278
        $commentEvents->dispatchService($service);
279
        $commentEvents->dispatchImage($service);
280
281
        // Now, let's configure the reverse proxy
282
        if ($variant === 'apache') {
283
            $commentEvents->dispatchNewVirtualHost($serviceName);
284
        }
285
286
        return null;
287
    }
288
289
    /**
290
     * @return array[] An array with 3 keys: phpVersions, variants and nodeVersions
291
     */
292
    private function getAvailableVersionParts() : array
293
    {
294
        $registryClient = new RegistryClient();
295
        $tags = $registryClient->getImageTagsOnDockerHub('thecodingmachine/php');
296
297
        $phpVersions = [];
298
        $variants = [];
299
        $nodeVersions = [];
300
301
        foreach ($tags as $tag) {
302
            $parts = \explode('-', $tag);
303
            if (count($parts) < 3) {
304
                continue;
305
            }
306
            if ($parts[1] !== 'v1') {
307
                continue;
308
            }
309
            $phpVersions[$parts[0]] = true;
310
            $variants[$parts[2]] = true;
311
            if (isset($parts[3])) {
312
                $nodeVersions[$parts[3]] = true;
313
            }
314
        }
315
316
        $phpVersions = \array_keys($phpVersions);
317
        $variants = \array_keys($variants);
318
        $nodeVersions = \array_keys($nodeVersions);
319
320
        rsort($phpVersions);
321
        sort($variants);
322
        rsort($nodeVersions, SORT_NUMERIC);
323
324
        return [
325
            'phpVersions' => $phpVersions,
326
            'variants' => $variants,
327
            'nodeVersions' => $nodeVersions,
328
        ];
329
    }
330
}
331