PhiremockServerCommand::processFileExpectations()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 11
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 8
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 11
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
                self::DEBUG_HELP_MESSAGE
85
            )
86
            ->addOption(
87
                'config-path',
88
                'c',
89
                InputOption::VALUE_REQUIRED,
90
                self::CONFIG_PATH_HELP_MESSAGE
91
            )
92
            ->addOption(
93
                'factory-class',
94
                'f',
95
                InputOption::VALUE_REQUIRED,
96
                self::FACTORY_CLASS_HELP_MESSAGE
97
            )
98
            ->addOption(
99
                'certificate',
100
                't',
101
                InputOption::VALUE_REQUIRED,
102
                self::CERTIFICATE_HELP_MESSAGE
103
            )
104
            ->addOption(
105
                'certificate-key',
106
                'k',
107
                InputOption::VALUE_REQUIRED,
108
                self::CERTIFICATE_KEY_HELP_MESSAGE
109
            )
110
            ->addOption(
111
                'cert-passphrase',
112
                's',
113
                InputOption::VALUE_REQUIRED,
114
                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
    protected function getConfigPath(InputInterface $input): ?Directory
137
    {
138
        $configPath = null;
139
        $configPathOptionValue = $input->getOption('config-path');
140
        if ($configPathOptionValue) {
141
            $configPath = new Directory($configPathOptionValue);
142
        }
143
144
        return $configPath;
145
    }
146
147
    /** @throws Exception */
148
    private function createPhiremockPathIfNotExists(): void
149
    {
150
        $defaultExpectationsPath = ConfigBuilder::getDefaultExpectationsDir();
151
        if (!$defaultExpectationsPath->exists()) {
152
            $defaultExpectationsPath->create();
153
        } elseif (!$defaultExpectationsPath->isDirectory()) {
154
            throw new Exception('Expectations path must be a directory');
155
        }
156
    }
157
158
    /** @throws Exception */
159
    private function startHttpServer(Config $config): void
160
    {
161
        $this->httpServer = $this->factory->createHttpServer();
162
        $this->setUpHandlers();
163
        $this->httpServer->listen($config->getInterfaceIp(), $config->getPort(), $config->getSecureOptions());
164
    }
165
166
    private function initializeLogger(Config $config): void
167
    {
168
        $this->logger = $this->factory->createLogger();
169
        $this->logger->info(
170
            sprintf(
171
                '[%s] Starting Phiremock%s...',
172
                date('Y-m-d H:i:s'),
173
                ($config->isDebugMode() ? ' in debug mode' : '')
174
            )
175
        );
176
    }
177
178
    /** @throws Exception */
179
    private function processFileExpectations(Config $config): void
180
    {
181
        $expectationsDir = $config->getExpectationsPath()->asString();
182
        $this->logger->debug(
183
            sprintf(
184
                'Phiremock\'s expectation dir is set to: %s',
185
                $this->factory->createFileSystemService()->getRealPath($expectationsDir))
186
        );
187
        $this->factory
188
            ->createFileExpectationsLoader()
189
            ->loadExpectationsFromDirectory($expectationsDir);
190
    }
191
192
    /** @throws ErrorException */
193
    private function setUpHandlers(): void
194
    {
195
        $handleTermination = function () {
196
            $this->logger->info('Stopping Phiremock...');
197
            $this->httpServer->shutdown();
198
            $this->logger->info('Bye bye');
199
        };
200
201
        $this->logger->debug('Registering shutdown function');
202
        register_shutdown_function($handleTermination);
203
204
        if (\function_exists('pcntl_signal')) {
205
            $this->logger->debug('PCNTL present: Installing signal handlers');
206
            pcntl_signal(\SIGTERM, function () { exit(0); });
207
        }
208
209
        $errorHandler = function ($severity, $message, $file, $line) {
210
            $errorInformation = sprintf('%s:%s (%s)', $file, $line, $message);
211
            if ($this->isError($severity)) {
212
                $this->logger->error($errorInformation);
213
                throw new ErrorException($message, 0, $severity, $file, $line);
214
            }
215
            $this->logger->warning($errorInformation);
216
217
            return false;
218
        };
219
        set_error_handler($errorHandler);
220
    }
221
222
    private function isError(int $severity): bool
223
    {
224
        return \in_array(
225
            $severity,
226
            [
227
                \E_COMPILE_ERROR,
228
                \E_CORE_ERROR,
229
                \E_USER_ERROR,
230
                \E_PARSE,
231
                \E_ERROR,
232
            ],
233
            true
234
        );
235
    }
236
237
    /** @return string[] */
238
    private function getCommandLineOptions(InputInterface $input): array
239
    {
240
        $cliConfig = [];
241
        foreach (Config::CONFIG_OPTIONS as $configOption) {
242
            $optionValue = $input->getOption($configOption);
243
            if ($optionValue) {
244
                $cliConfig[$configOption] = (string) $optionValue;
245
            }
246
        }
247
248
        return $cliConfig;
249
    }
250
}
251