Passed
Push — master ( 0b7dea...b9611f )
by Mariano
04:34
created

PhiremockServerCommand::getConfigPath()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 5
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 8
rs 10
1
<?php
2
/**
3
 * This file is part of Phiremock.
4
 *
5
 * Phiremock is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU Lesser General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 *
10
 * Phiremock is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 * GNU General Public License for more details.
14
 *
15
 * You should have received a copy of the GNU General Public License
16
 * along with Phiremock.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18
19
namespace Mcustiel\Phiremock\Server\Cli\Commands;
20
21
use ErrorException;
22
use Exception;
23
use Mcustiel\Phiremock\Server\Factory\Factory;
24
use Mcustiel\Phiremock\Server\Http\ServerInterface;
25
use Mcustiel\Phiremock\Server\Utils\Config\Config;
26
use Mcustiel\Phiremock\Server\Utils\Config\ConfigBuilder;
27
use Mcustiel\Phiremock\Server\Utils\Config\Directory;
28
use Psr\Log\LoggerInterface;
29
use Symfony\Component\Console\Command\Command;
30
use Symfony\Component\Console\Input\InputInterface;
31
use Symfony\Component\Console\Input\InputOption;
32
use Symfony\Component\Console\Output\OutputInterface;
33
34
class PhiremockServerCommand extends Command
35
{
36
    const IP_HELP_MESSAGE = 'IP address of the interface where Phiremock must list for connections.';
37
    const PORT_HELP_MESSAGE = 'Port where Phiremock must list for connections.';
38
    const EXPECTATIONS_DIR_HELP_MESSAGE = 'Directory in which to search for expectation definition files.';
39
    const DEBUG_HELP_MESSAGE = 'Sets debug mode.';
40
    const CONFIG_PATH_HELP_MESSAGE = 'Directory in which to search for configuration files. Default: current directory.';
41
    const FACTORY_CLASS_HELP_MESSAGE = 'Factory class to use. It must inherit from: ' . Factory::class;
42
    const CERTIFICATE_HELP_MESSAGE = 'Path to the local certificate for secure connection';
43
    const CERTIFICATE_KEY_HELP_MESSAGE = 'Path to the local certificate key for secure connection';
44
    const PASSPHRASE_HELP_MESSAGE = 'Passphrase if the local certificate is encrypted';
45
46
    /** @var Factory */
47
    private $factory;
48
    /** @var LoggerInterface */
49
    private $logger;
50
    /** @var ServerInterface */
51
    private $httpServer;
52
53
    public function __construct()
54
    {
55
        parent::__construct('run');
56
    }
57
58
    protected function configure(): void
59
    {
60
        $this->setDescription('Runs Phiremock server')
61
            ->setHelp('This is the main command to run Phiremock as a HTTP server.')
62
            ->addOption(
63
                'ip',
64
                'i',
65
                InputOption::VALUE_REQUIRED,
66
                self::IP_HELP_MESSAGE
67
            )
68
            ->addOption(
69
                'port',
70
                'p',
71
                InputOption::VALUE_REQUIRED,
72
                self::PORT_HELP_MESSAGE
73
            )
74
            ->addOption(
75
                'expectations-dir',
76
                'e',
77
                InputOption::VALUE_REQUIRED,
78
                self::EXPECTATIONS_DIR_HELP_MESSAGE
79
            )
80
            ->addOption(
81
                'debug',
82
                'd',
83
                InputOption::VALUE_NONE,
84
                sprintf(self::DEBUG_HELP_MESSAGE)
85
            )
86
            ->addOption(
87
                'config-path',
88
                'c',
89
                InputOption::VALUE_REQUIRED,
90
                sprintf(self::CONFIG_PATH_HELP_MESSAGE)
91
            )
92
            ->addOption(
93
                'factory-class',
94
                'f',
95
                InputOption::VALUE_REQUIRED,
96
                sprintf(self::FACTORY_CLASS_HELP_MESSAGE)
97
            )
98
            ->addOption(
99
                'certificate',
100
                't',
101
                InputOption::VALUE_REQUIRED,
102
                sprintf(self::CERTIFICATE_HELP_MESSAGE)
103
            )
104
            ->addOption(
105
                'certificate-key',
106
                'k',
107
                InputOption::VALUE_REQUIRED,
108
                sprintf(self::CERTIFICATE_KEY_HELP_MESSAGE)
109
            )
110
            ->addOption(
111
                'cert-passphrase',
112
                's',
113
                InputOption::VALUE_REQUIRED,
114
                sprintf(self::PASSPHRASE_HELP_MESSAGE)
115
            );
116
    }
117
118
    /** @throws Exception */
119
    protected function execute(InputInterface $input, OutputInterface $output): int
120
    {
121
        $this->createPhiremockPathIfNotExists();
122
123
        $configPath = $this->getConfigPath($input);
124
        $cliConfig = $this->getCommandLineOptions($input);
125
126
        $config = (new ConfigBuilder($configPath))->build($cliConfig);
127
128
        $this->factory = $config->getFactoryClassName()->asInstance($config);
129
        $this->initializeLogger($config);
130
        $this->processFileExpectations($config);
131
        $this->startHttpServer($config);
132
133
        return 0;
134
    }
135
136
    /** @throws Exception */
137
    private function createPhiremockPathIfNotExists(): void
138
    {
139
        $defaultExpectationsPath = ConfigBuilder::getDefaultExpectationsDir();
140
        if (!$defaultExpectationsPath->exists()) {
141
            $defaultExpectationsPath->create();
142
        } elseif (!$defaultExpectationsPath->isDirectory()) {
143
            throw new Exception('Expectations path must be a directory');
144
        }
145
    }
146
147
    /** @throws Exception */
148
    private function startHttpServer(Config $config): void
149
    {
150
        $this->httpServer = $this->factory->createHttpServer();
151
        $this->setUpHandlers();
152
        $this->httpServer->listen($config->getInterfaceIp(), $config->getPort(), $config->getSecureOptions());
153
    }
154
155
    private function initializeLogger(Config $config): void
156
    {
157
        $this->logger = $this->factory->createLogger();
158
        $this->logger->info(
159
            sprintf(
160
                '[%s] Starting Phiremock%s...',
161
                date('Y-m-d H:i:s'),
162
                ($config->isDebugMode() ? ' in debug mode' : '')
163
            )
164
        );
165
    }
166
167
    /** @throws Exception */
168
    private function processFileExpectations(Config $config): void
169
    {
170
        $expectationsDir = $config->getExpectationsPath()->asString();
171
        $this->logger->debug(
172
            sprintf(
173
                'Phiremock\'s expectation dir is set to: %s',
174
                $this->factory->createFileSystemService()->getRealPath($expectationsDir))
175
        );
176
        $this->factory
177
            ->createFileExpectationsLoader()
178
            ->loadExpectationsFromDirectory($expectationsDir);
179
    }
180
181
    /** @throws ErrorException */
182
    private function setUpHandlers(): void
183
    {
184
        $handleTermination = function () {
185
            $this->logger->info('Stopping Phiremock...');
186
            $this->httpServer->shutdown();
187
            $this->logger->info('Bye bye');
188
        };
189
190
        $this->logger->debug('Registering shutdown function');
191
        register_shutdown_function($handleTermination);
192
193
        if (function_exists('pcntl_signal')) {
194
            $this->logger->debug('PCNTL present: Installing signal handlers');
195
            pcntl_signal(SIGTERM, function () { exit(0); });
196
        }
197
198
        $errorHandler = function ($severity, $message, $file, $line) {
199
            $errorInformation = sprintf('%s:%s (%s)', $file, $line, $message);
200
            if ($this->isError($severity)) {
201
                $this->logger->error($errorInformation);
202
                throw new ErrorException($message, 0, $severity, $file, $line);
203
            }
204
            $this->logger->warning($errorInformation);
205
206
            return false;
207
        };
208
        set_error_handler($errorHandler);
209
    }
210
211
    private function isError(int $severity): bool
212
    {
213
        return in_array(
214
            $severity,
215
            [
216
                E_COMPILE_ERROR,
217
                E_CORE_ERROR,
218
                E_USER_ERROR,
219
                E_PARSE,
220
                E_ERROR,
221
            ],
222
            true
223
        );
224
    }
225
226
    /** @return string[] */
227
    private function getCommandLineOptions(InputInterface $input): array
228
    {
229
        $cliConfig = [];
230
        foreach (Config::CONFIG_OPTIONS as $configOption) {
231
            $optionValue = $input->getOption($configOption);
232
            if ($optionValue) {
233
                $cliConfig[$configOption] = (string)$optionValue;
234
            }
235
        }
236
        return $cliConfig;
237
    }
238
239
    /**
240
     * @param InputInterface $input
241
     * @return Directory|null
242
     */
243
    protected function getConfigPath(InputInterface $input): ?Directory
244
    {
245
        $configPath = null;
246
        $configPathOptionValue = $input->getOption('config-path');
247
        if ($configPathOptionValue) {
248
            $configPath = new Directory($configPathOptionValue);
249
        }
250
        return $configPath;
251
    }
252
}
253