AddEvent::getAvailableVersionParts()   A
last analyzed

Complexity

Conditions 5
Paths 5

Size

Total Lines 26
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 20
nc 5
nop 0
dl 0
loc 26
rs 9.2888
c 0
b 0
f 0
1
<?php
2
3
namespace TheAentMachine\AentPhp\Event;
4
5
use Safe\Exceptions\ArrayException;
6
use Safe\Exceptions\FilesystemException;
7
use Safe\Exceptions\StringsException;
8
use TheAentMachine\Aent\Event\Service\AbstractServiceAddEvent;
9
use TheAentMachine\Aent\Event\Service\Model\Environments;
10
use TheAentMachine\Aent\Event\Service\Model\ServiceState;
11
use TheAentMachine\Aenthill\Pheromone;
12
use TheAentMachine\Exception\MissingEnvironmentVariableException;
13
use TheAentMachine\Prompt\Helper\ValidatorHelper;
14
use TheAentMachine\Registry\RegistryClient;
15
use TheAentMachine\Service\Service;
16
use TheAentMachine\Service\Volume\BindVolume;
17
use TheAentMachine\Service\Volume\NamedVolume;
18
use function Safe\rsort;
19
use function Safe\sprintf;
20
21
final class AddEvent extends AbstractServiceAddEvent
22
{
23
    /**
24
     * @param Environments $environments
25
     * @return ServiceState[]
26
     * @throws MissingEnvironmentVariableException
27
     * @throws ArrayException
28
     * @throws StringsException
29
     * @throws FilesystemException
30
     */
31
    protected function createServices(Environments $environments): array
32
    {
33
        $service = new Service();
34
        $service->setServiceName($this->prompt->getPromptHelper()->getServiceName());
35
        $service->setImage($this->getImage());
36
        $rootDirectoryVolume = $this->getRootDirectoryVolume();
37
        $service->addBindVolume('./' . $rootDirectoryVolume->getSource(), $rootDirectoryVolume->getTarget());
38
        $apacheDocumentRoot = $this->getApacheDocumentRoot($rootDirectoryVolume->getSource());
39
        if (!empty($apacheDocumentRoot)) {
40
            $service->addImageEnvVariable('APACHE_DOCUMENT_ROOT', $apacheDocumentRoot);
41
        }
42
        $namedVolumes = $this->getNamedVolumes($rootDirectoryVolume->getSource());
43
        foreach ($namedVolumes as $namedVolume) {
44
            $service->addNamedVolume($namedVolume->getSource(), $namedVolume->getTarget());
45
        }
46
        $extensions = $this->getPHPExtensions();
47
        foreach ($extensions as $extension) {
48
            $service->addImageEnvVariable('PHP_EXTENSION_' . \strtoupper($extension), '1');
49
        }
50
        $service->addImageEnvVariable('PHP_INI_MEMORY_LIMIT', '1G');
51
        $service->addImageEnvVariable('PHP_INI_UPLOAD_MAX_FILESIZE', '50M');
52
        $service->addImageEnvVariable('PHP_INI_POST_MAX_SIZE', '50M');
53
        $service->addInternalPort(80);
54
        $service->addVirtualHost(80);
55
        $service->setNeedBuild(true);
56
        $developmentVersion = clone $service;
57
        $developmentVersion->addContainerEnvVariable('STARTUP_COMMAND_1', 'composer install', 'This command will be automatically launched on container startup');
58
        $remoteVersion = clone $service;
59
        $remoteVersion->addDockerfileCommand(sprintf('FROM %s', $service->getImage()));
60
        $remoteVersion->addDockerfileCommand(sprintf('COPY --chown=docker:docker %s .', $rootDirectoryVolume->getSource()));
61
        $remoteVersion->addDockerfileCommand('RUN composer install');
62
        if (strpos($service->getImage() ?? '', 'node') !== false) {
63
            $remoteVersion->addDockerfileCommand('RUN yarn install');
64
        }
65
        $serviceState = new ServiceState($developmentVersion, $remoteVersion, $remoteVersion);
66
        return [$serviceState];
67
    }
68
69
    /**
70
     * @return string
71
     * @throws ArrayException
72
     */
73
    private function getImage(): string
74
    {
75
        [
76
            'phpVersions' => $phpVersions,
77
            'nodeVersions' => $nodeVersions,
78
        ] = $this->getAvailableVersionParts();
79
        $phpVersion = $this->prompt->select("\nPHP version", $phpVersions, null, $phpVersions[0], true) ?? '';
80
        $variant = 'apache';
81
        $withNode = $this->prompt->confirm("\nDo you want to use Node.js for building your frontend source code?", null, null, true);
82
        $node = '';
83
        if ($withNode) {
84
            $node = $this->prompt->select("\nNode.js version", $nodeVersions, null, $nodeVersions[0], true) ?? '';
85
            $this->output->writeln("\nšŸ‘Œ Alright, I'm going to use PHP <info>$phpVersion</info> with Node.js <info>$node</info>!");
86
            $node = '-' . $node;
87
        } else {
88
            $this->output->writeln("\nšŸ‘Œ Alright, I'm going to use PHP <info>$phpVersion</info> without Node.js!");
89
        }
90
        return "thecodingmachine/php:$phpVersion-v1-$variant$node";
91
    }
92
93
    /**
94
     * @return mixed[] An array with 2 keys: phpVersions and nodeVersions
95
     * @throws ArrayException
96
     */
97
    private function getAvailableVersionParts() : array
98
    {
99
        $registryClient = new RegistryClient();
100
        $tags = $registryClient->getImageTagsOnDockerHub('thecodingmachine/php');
101
        $phpVersions = [];
102
        $nodeVersions = [];
103
        foreach ($tags as $tag) {
104
            $parts = \explode('-', $tag);
105
            if (count($parts) < 3) {
106
                continue;
107
            }
108
            if ($parts[1] !== 'v1') {
109
                continue;
110
            }
111
            $phpVersions[$parts[0]] = true;
112
            if (isset($parts[3])) {
113
                $nodeVersions[$parts[3]] = true;
114
            }
115
        }
116
        $phpVersions = \array_keys($phpVersions);
117
        $nodeVersions = \array_keys($nodeVersions);
118
        rsort($phpVersions);
119
        rsort($nodeVersions, SORT_NUMERIC);
120
        return [
121
            'phpVersions' => $phpVersions,
122
            'nodeVersions' => $nodeVersions,
123
        ];
124
    }
125
126
    /**
127
     * @return BindVolume
128
     */
129
    private function getRootDirectoryVolume(): BindVolume
130
    {
131
        $text = "\n<info>PHP application directory</info> (relative to the project root directory)";
132
        $helpText = "Your <info>PHP application directory</info> is typically the directory that contains your <info>composer.json file</info>. It must be relative to the project root directory.";
133
        $source = $this->prompt->input($text, $helpText, null, true, ValidatorHelper::getRelativePathValidator()) ?? '';
134
        return new BindVolume($source, '/var/www/html');
135
    }
136
137
    /**
138
     * @param string $rootDirectory
139
     * @return null|string
140
     * @throws MissingEnvironmentVariableException
141
     * @throws StringsException
142
     */
143
    private function getApacheDocumentRoot(string $rootDirectory): ?string
144
    {
145
        $text = "\n<info>Apache document root</info> (relative to the PHP application directory - leave empty if it's the PHP application directory)";
146
        $helpText = sprintf(
147
            "The <info>Apache document root</info> is typically the directory that contains your <info>index.php</info> file. It must be relative to the PHP application directory (%s/%s).",
148
            Pheromone::getHostProjectDirectory(),
149
            $rootDirectory
150
        );
151
        return $this->prompt->input($text, $helpText, null, false, ValidatorHelper::getRelativePathValidator()) ?? '';
152
    }
153
154
    /**
155
     * @param string $rootDirectory
156
     * @return NamedVolume[]
157
     */
158
    private function getNamedVolumes(string $rootDirectory): array
159
    {
160
        $this->output->writeln("\nNow, we need to know if there are directories you want to store <info>out of the container</info>.");
161
        $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>.');
162
        $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.');
163
        $namedVolumes = [];
164
        do {
165
            $text = "\nDirectory (relative to root directory) you want to mount out of the container (keep empty to skip)";
166
            $dir = $this->prompt->input($text, null, null, false, ValidatorHelper::getRelativePathValidator());
167
            if (!empty($dir)) {
168
                $namedVolumes[] = new NamedVolume($dir . '_data', "/var/www/html/$rootDirectory/$dir");
169
            }
170
        } while (!empty($dir));
171
        return $namedVolumes;
172
    }
173
174
    /**
175
     * @return string[]
176
     */
177
    private function getPHPExtensions(): array
178
    {
179
        $availableExtensions = ['amqp', 'ast', 'bcmath', 'bz2', 'calendar', 'dba', 'enchant', 'ev', 'event', 'exif',
180
            'gd', 'gettext', 'gmp', 'igbinary', 'imap', 'intl', 'ldap', 'mcrypt', 'memcached', 'mongodb', 'pcntl',
181
            'pdo_dblib', 'pdo_pgsql', 'pgsql', 'pspell', 'shmop', 'snmp', 'sockets', 'sysvmsg', 'sysvsem', 'sysvshm',
182
            'tidy', 'wddx', 'weakref', 'xdebug', 'xmlrpc', 'xsl', 'yaml'];
183
        $this->output->writeln("\nBy default, the following extensions are enabled:");
184
        $this->output->writeln('<info>apcu mysqli opcache pdo pdo_mysql redis zip soap mbstring ftp mysqlnd</info>');
185
        $this->output->writeln('You can select more extensions below:');
186
        $this->output->writeln('<info>' . \implode(' ', $availableExtensions) . '</info>');
187
        $extensions = [];
188
        do {
189
            $text = "\nExtension you want to install (keep empty to skip)";
190
            $extension = $this->prompt->autocompleter(
191
                $text,
192
                $availableExtensions,
193
                null,
194
                null,
195
                false,
196
                function (string $value) use ($availableExtensions) {
197
                    if (\trim($value) !== '' && !\in_array($value, $availableExtensions, true)) {
198
                        throw new \InvalidArgumentException('Unknown extension ' . $value);
199
                    }
200
                    return \trim($value);
201
                }
202
            );
203
            if (!empty($extension)) {
204
                $extensions[] = $extension;
205
            }
206
        } while (!empty($extension));
207
        $this->output->writeln("\nšŸ‘Œ Alright, I'm going to enable the following extensions: <info>apcu mysqli opcache pdo pdo_mysql redis zip soap mbstring ftp mysqlnd " . \implode(' ', $extensions) . '</info>');
208
        return $extensions;
209
    }
210
}
211