MockServer::startServer()   A
last analyzed

Complexity

Conditions 3
Paths 2

Size

Total Lines 21
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 3

Importance

Changes 0
Metric Value
eloc 11
dl 0
loc 21
ccs 11
cts 11
cp 1
rs 9.9
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 3
1
<?php declare(strict_types=1);
2
3
namespace EdmondsCommerce\MockServer;
4
5
/**
6
 * Class MockServer
7
 *
8
 * @package EdmondsCommerce\MockServer
9
 * @SuppressWarnings(PHPMD.StaticAccess)
10
 */
11
class MockServer
12
{
13
    public const LOG_FILE      = 'mockserver.log';
14
    public const REQUEST_FILE  = 'request.json';
15
    public const RESPONSE_FILE = 'response.json';
16
17
    /**
18
     * @var string
19
     */
20
    private static $logsPath;
21
22
    /**
23
     * @var string
24
     */
25
    private $routerPath;
26
27
    /**
28
     * @var string
29
     */
30
    private $ipAddress;
31
32
    /**
33
     * @var int
34
     */
35
    private $port;
36
37
    /**
38
     * @var string
39
     */
40
    private $htdocsPath;
41
42
43
    /**
44
     * MockServer constructor.
45
     *
46
     * @SuppressWarnings(PHPMD.StaticAccess)
47
     * @param string $routerPath
48
     * @param string $htdocsPath
49
     * @param string $ipAddress
50
     * @param int    $port
51
     *
52
     * @throws \Exception
53
     */
54 3
    public function __construct(
55
        string $routerPath,
56
        string $htdocsPath = '',
57
        string $ipAddress = null,
58
        int $port = null
59
    ) {
60 3
        if (!is_file($routerPath)) {
61
            throw new \RuntimeException('Router file does not exist: "'.$routerPath.'"');
62
        }
63 3
        $this->routerPath = realpath($routerPath);
64
65 3
        $this->htdocsPath = trim($htdocsPath ?: \dirname($this->routerPath));
66 3
        if (!is_dir($this->htdocsPath)) {
67
            throw new \RuntimeException('Htdocs folder does not exist: "'.$this->htdocsPath.'"');
68
        }
69 3
        $this->ipAddress = trim($ipAddress ?? MockServerConfig::DEFAULT_IP);
70 3
        $this->port      = $port ?? MockServerConfig::DEFAULT_PORT;
71 3
        $this->clearLogs();
72 3
    }
73
74 3
    public function __destruct()
75
    {
76 3
        $this->stopServer();
77 3
    }
78
79
    /**
80
     * Sets up a temporary file and returns the path to it
81
     *
82
     * @return string
83
     * @throws \Exception
84
     */
85 10
    public static function getLogsPath(): string
86
    {
87 10
        if (null !== self::$logsPath) {
0 ignored issues
show
introduced by
The condition null !== self::logsPath is always true.
Loading history...
88 9
            return self::$logsPath;
89
        }
90 1
        self::$logsPath = MockServerConfig::getLogsPath();
91 1
        if (!is_dir(self::$logsPath)
92 1
            && !(mkdir(self::$logsPath, 0777, true) && is_dir(self::$logsPath))
93
        ) {
94
            throw new \RuntimeException(sprintf('Directory "%s" was not created', self::$logsPath));
95
        }
96
97 1
        return self::$logsPath;
98
    }
99
100
    /**
101
     * Get the Server start command
102
     * - Supports running in the background (default) or in the foreground
103
     * - Supports running without Xdebug (default) or with Xdebug enabled allowing you to debug the mock server itself
104
     *
105
     * @param bool $background
106
     *
107
     * @param bool $xdebug
108
     *
109
     * @return string
110
     * @throws \Exception
111
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
112
     */
113 3
    public function getStartCommand(bool $background = true, $xdebug = false): string
114
    {
115 3
        $logFilePath      = self::getLogsPath().'/'.self::LOG_FILE;
116 3
        $nohup            = '';
117 3
        $detatch          = '';
118 3
        $noXdebugFunction = '';
119 3
        $iniPathConfig    = '';
120 3
        if (true === $background) {
121 3
            $nohup   = ' nohup ';
122 3
            $detatch = ' > \''.$logFilePath.'\' 2>&1 &';
123
        }
124 3
        if (true !== $xdebug) {
125 3
            $iniFile       = '/tmp/phpNoXdebug.ini';
126 3
            $iniPathConfig = ' -n -c "'.$iniFile.'"';
127 3
            shell_exec(
128
                "php -i | grep '\.ini' "
129
                ."| grep -o -e '\(/[a-z0-9._-]\+\)\+\.ini' "
130
                .'| grep -v xdebug '
131 3
                ."| xargs awk 'FNR==1{print \"\"}1' > $iniFile"
132
            );
133
        }
134
135
        return $noXdebugFunction
136 3
               .'cd '.$this->htdocsPath.';'
137 3
               .$nohup
138 3
               .'php'
139 3
               .$iniPathConfig
140 3
               .' -d error_reporting=E_ALL'
141 3
               .' -d error_log=\''.$logFilePath.'\''
142 3
               .' -S '.$this->ipAddress.':'.$this->port.' '.$this->routerPath
143 3
               .$detatch;
144
    }
145
146
147
    /**
148
     * Start the mock web server
149
     *
150
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
151
     * @param bool $xdebug
152
     *
153
     * @return bool
154
     * @throws \Exception
155
     * @SuppressWarnings(PHPMD.BooleanArgumentFlag)
156
     */
157 3
    public function startServer(bool $xdebug = false): bool
158
    {
159
        //Stop the server if it is already running
160 3
        if ($this->isServerRunning()) {
161 1
            $this->stopServer();
162
        }
163 3
        $this->clearLogs();
164
165 3
        $startCommand = $this->getStartCommand(true, $xdebug);
166
167 3
        exec($startCommand, $commandOutput, $exitCode);
168
169
        //Sleep to allow the web server to start, need to keep this as low as we can to ensure tests don't take forever
170
        //Maximum attempts to try and connect before we fail out
171 3
        $totalAttempts      = 0;
172 3
        $maxTimeoutAttempts = 5;
173
        do {
174 3
            usleep(100000); // 0.1s
175 3
        } while (!$this->isServerRunning() && $totalAttempts++ < $maxTimeoutAttempts);
176
177 3
        return ($exitCode === 0);
178
    }
179
180
    /**
181
     * @throws \Exception
182
     */
183 4
    public function clearLogs(): void
184
    {
185 4
        $logsPath = self::getLogsPath();
186
        $files    = [
187 4
            self::LOG_FILE,
188 4
            self::REQUEST_FILE,
189 4
            self::RESPONSE_FILE,
190
        ];
191 4
        foreach ($files as $file) {
192 4
            file_put_contents($logsPath.$file, '');
193
        }
194 4
    }
195
196
197
    /**
198
     * Checks if the PHP server is running
199
     *
200
     * @return bool
201
     * @throws \Exception
202
     */
203 3
    public function isServerRunning(): bool
204
    {
205 3
        $pid = $this->getServerPID();
206
207 3
        return ($pid > 0);
208
    }
209
210
    /**
211
     * Gets the PHP server's PID
212
     *
213
     * @return int
214
     * @throws \Exception
215
     */
216 4
    public function getServerPID(): int
217
    {
218
        //-x Preg matches only on exact names instead of partial match
219
        //-f Matches against the process name AND the arguments for us to denote the web server from other PHP processes
220 4
        $command = 'pgrep -u "$(whoami),root" -f "php.*[-]S"';
221
222 4
        exec($command, $outputArray, $exitCode);
223 4
        $output = implode("\n", $outputArray);
224
225 4
        if ($exitCode !== 0 && count($outputArray) > 1) {
226
            throw new \RuntimeException(
227
                'Unsuccessful exit code returned: '.$exitCode.', output: '
228
                .$outputArray
229
            );
230
        }
231
232 4
        if (count($outputArray) > 1) {
233
            exec('ps $('.$command.')', $psOutput);
234
            $psOutput = implode("\n", $psOutput);
235
            throw new \RuntimeException('Found multiple instances of the PHP server:'.$output."\nps output:".$psOutput);
236
        }
237
238
        //Not found
239 4
        if ($exitCode === 1 && count($outputArray) === 0) {
240 3
            return 0;
241
        }
242
243 3
        $pid = trim($output);
244
245 3
        if (is_numeric($pid)) {
246 3
            return (int)$pid;
247
        }
248
249
        throw new \RuntimeException('Could not find PID for PHP Server: '.$output);
250
    }
251
252
    /**
253
     * @throws \Exception
254
     */
255 4
    public function stopServer(): void
256
    {
257
258 4
        $pid = $this->getServerPID();
259 4
        if ($pid === 0) {
260 1
            return;
261
        }
262 3
        $command = sprintf('kill %d 2>&1', $pid);
263 3
        exec($command, $output, $resultCode);
264 3
        $output = implode("\n", $output);
265 3
        if (0 !== $resultCode) {
266
            if (false !== stripos($output, 'no such process')) {
267
                return;
268
            }
269
            throw new \RuntimeException('Failed stopping server: '.$output);
270
        }
271 3
    }
272
273 5
    public function getBaseUrl(): string
274
    {
275 5
        return sprintf('http://%s:%d', $this->ipAddress, $this->port);
276
    }
277
278 5
    public function getUrl($uri): string
279
    {
280 5
        if ('/' !== $uri[0]) {
281 1
            $uri = "/$uri";
282
        }
283
284 5
        return $this->getBaseUrl().$uri;
285
    }
286
}
287