Completed
Push — master ( 61a4ab...a43b76 )
by Julien
07:08 queued 04:33
created

AddEvent::getImage()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 14
nc 2
nop 0
dl 0
loc 18
rs 9.7998
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
        return $this->prompt->getPromptHelper()->getBindVolume($text, '/var/www/html', $helpText);
134
    }
135
136
    /**
137
     * @param string $rootDirectory
138
     * @return null|string
139
     * @throws MissingEnvironmentVariableException
140
     * @throws StringsException
141
     */
142
    private function getApacheDocumentRoot(string $rootDirectory): ?string
143
    {
144
        $text = "\n<info>Apache document root</info> (relative to the PHP application directory - leave empty if it's the PHP application directory)";
145
        $helpText = sprintf(
146
            "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).",
147
            Pheromone::getHostProjectDirectory(),
148
            $rootDirectory
149
        );
150
        return $this->prompt->input($text, $helpText, null, false, ValidatorHelper::getAlphaValidator()) ?? '';
151
    }
152
153
    /**
154
     * @param string $rootDirectory
155
     * @return NamedVolume[]
156
     */
157
    private function getNamedVolumes(string $rootDirectory): array
158
    {
159
        $this->output->writeln("\nNow, we need to know if there are directories you want to store <info>out of the container</info>.");
160
        $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>.');
161
        $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.');
162
        $namedVolumes = [];
163
        do {
164
            $text = "\nDirectory (relative to root directory) you want to mount out of the container (keep empty to skip)";
165
            $dir = $this->prompt->input($text, null, null, false, ValidatorHelper::getAlphaValidator());
166
            if (!empty($dir)) {
167
                $namedVolumes[] = new NamedVolume($dir . '_data', $rootDirectory . '/' . $dir);
168
            }
169
        } while (!empty($dir));
170
        return $namedVolumes;
171
    }
172
173
    /**
174
     * @return string[]
175
     */
176
    private function getPHPExtensions(): array
177
    {
178
        $availableExtensions = ['amqp', 'ast', 'bcmath', 'bz2', 'calendar', 'dba', 'enchant', 'ev', 'event', 'exif',
179
            'gd', 'gettext', 'gmp', 'igbinary', 'imap', 'intl', 'ldap', 'mcrypt', 'memcached', 'mongodb', 'pcntl',
180
            'pdo_dblib', 'pdo_pgsql', 'pgsql', 'pspell', 'shmop', 'snmp', 'sockets', 'sysvmsg', 'sysvsem', 'sysvshm',
181
            'tidy', 'wddx', 'weakref', 'xdebug', 'xmlrpc', 'xsl', 'yaml'];
182
        $this->output->writeln("\nBy default, the following extensions are enabled:");
183
        $this->output->writeln('<info>apcu mysqli opcache pdo pdo_mysql redis zip soap mbstring ftp mysqlnd</info>');
184
        $this->output->writeln('You can select more extensions below:');
185
        $this->output->writeln('<info>' . \implode(' ', $availableExtensions) . '</info>');
186
        $extensions = [];
187
        do {
188
            $text = "\nExtension you want to install (keep empty to skip)";
189
            $extension = $this->prompt->autocompleter(
190
                $text,
191
                $availableExtensions,
192
                null,
193
                null,
194
                false,
195
                function (string $value) use ($availableExtensions) {
196
                    if (\trim($value) !== '' && !\in_array($value, $availableExtensions, true)) {
197
                        throw new \InvalidArgumentException('Unknown extension ' . $value);
198
                    }
199
                    return \trim($value);
200
                }
201
            );
202
            if (!empty($extension)) {
203
                $extensions[] = $extension;
204
            }
205
        } while (!empty($extension));
206
        $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>');
207
        return $extensions;
208
    }
209
}
210