Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
43 / 43
91.84% covered (success)
91.84%
45 / 49
57.14% covered (warning)
57.14%
20 / 35
50.00% covered (danger)
50.00%
4 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
FrankenPhpWorker
100.00% covered (success)
100.00%
43 / 43
91.84% covered (success)
91.84%
45 / 49
57.14% covered (warning)
57.14%
20 / 35
100.00% covered (success)
100.00%
8 / 8
55.71
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 configure
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 serve
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
5 / 5
75.00% covered (warning)
75.00%
3 / 4
100.00% covered (success)
100.00%
1 / 1
2.06
 retires
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
9 / 9
55.56% covered (warning)
55.56%
5 / 9
100.00% covered (success)
100.00%
1 / 1
7.19
 requestsSetting
100.00% covered (success)
100.00%
6 / 6
87.50% covered (warning)
87.50%
7 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
3.58
 memorySetting
100.00% covered (success)
100.00%
8 / 8
87.50% covered (warning)
87.50%
7 / 8
60.00% covered (warning)
60.00%
3 / 5
100.00% covered (success)
100.00%
1 / 1
3.58
 setting
100.00% covered (success)
100.00%
3 / 3
90.00% covered (success)
90.00%
9 / 10
33.33% covered (danger)
33.33%
2 / 6
100.00% covered (success)
100.00%
1 / 1
5.67
 defaultMaxMemory
100.00% covered (success)
100.00%
3 / 3
85.71% covered (warning)
85.71%
6 / 7
50.00% covered (danger)
50.00%
2 / 4
100.00% covered (success)
100.00%
1 / 1
4.12
1<?php
2
3declare(strict_types=1);
4
5namespace Celema\Core\Runtime;
6
7use Celema\Core\App;
8use Celema\Core\Exception\RuntimeException;
9use Closure;
10
11/**
12 * Handles requests in a FrankenPHP worker until FrankenPHP stops it or it
13 * retires: after a request that left the app unusable, after a configured
14 * number of requests, or above a memory threshold. FrankenPHP then starts
15 * a fresh worker.
16 *
17 * @internal
18 */
19final class FrankenPhpWorker
20{
21    public const string MAX_REQUESTS = 'CELEMA_WORKER_MAX_REQUESTS';
22    public const string MAX_MEMORY = 'CELEMA_WORKER_MAX_MEMORY';
23
24    /**
25     * @param Closure(callable(): void): bool $handleRequest Waits for a request and runs the
26     *     callback for it; returns false when the worker should stop (frankenphp_handle_request).
27     */
28    public function __construct(
29        private readonly Closure $handleRequest,
30        public readonly int $maxRequests,
31        public readonly int $maxMemory,
32    ) {}
33
34    /**
35     * Explicit limits win over the worker's environment (for example the
36     * `env` directive of a Caddyfile worker block). Without either, the
37     * request limit is off and the memory limit is 80 % of memory_limit.
38     *
39     * @param Closure(callable(): void): bool $handleRequest
40     */
41    public static function configure(
42        Closure $handleRequest,
43        ?int $maxRequests = null,
44        ?int $maxMemory = null,
45    ): self {
46        return new self(
47            $handleRequest,
48            $maxRequests ?? self::requestsSetting() ?? 0,
49            $maxMemory ?? self::memorySetting() ?? self::defaultMaxMemory(),
50        );
51    }
52
53    public function serve(App $app): false
54    {
55        // A client that disconnects must not abort a request halfway, as
56        // its teardown has to run before the worker takes the next one.
57        ignore_user_abort(true);
58        $handled = 0;
59
60        do {
61            $running = ($this->handleRequest)(static function () use ($app): void {
62                // PHP's stat cache would keep file sizes and modification
63                // times from earlier requests.
64                clearstatcache();
65                $app->run();
66            });
67            $handled++;
68            gc_collect_cycles();
69        } while ($running && !$this->retires($app, $handled));
70
71        return false;
72    }
73
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
78                || $this->maxRequests > 0
79                && $handled >= $this->maxRequests
80                || $this->maxMemory > 0
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
84
85    private static function requestsSetting(): ?int
86    {
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
90            return null;
91        }
92
93        if (preg_match('/^\d+$/', $value) !== 1) {
94            throw new RuntimeException(self::MAX_REQUESTS . ' must be a number of requests, 0 for no limit');
95        }
96
97        return (int) $value;
98    }
99
100    private static function memorySetting(): ?int
101    {
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
105            return null;
106        }
107
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
109            throw new RuntimeException(
110                self::MAX_MEMORY . ' must be a number of bytes, optionally with K, M or G, 0 for no limit',
111            );
112        }
113
114        return ini_parse_quantity($value);
115    }
116
117    /** @return ?non-empty-string */
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
124    }
125
126    private static function defaultMaxMemory(): int
127    {
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
133}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

FrankenPhpWorker->__construct
29        private readonly Closure $handleRequest,
30        public readonly int $maxRequests,
31        public readonly int $maxMemory,
32    ) {}
FrankenPhpWorker->configure
42        Closure $handleRequest,
43        ?int $maxRequests = null,
44        ?int $maxMemory = null,
45    ): self {
46        return new self(
47            $handleRequest,
48            $maxRequests ?? self::requestsSetting() ?? 0,
49            $maxMemory ?? self::memorySetting() ?? self::defaultMaxMemory(),
50        );
51    }
FrankenPhpWorker->defaultMaxMemory
128        $limit = (string) ini_get('memory_limit');
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
129        $bytes = $limit === '' ? 0 : ini_parse_quantity($limit);
130
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
131        return $bytes > 0 ? intdiv($bytes * 4, 5) : 0;
132    }
FrankenPhpWorker->memorySetting
102        $value = self::setting(self::MAX_MEMORY);
103
104        if ($value === null) {
105            return null;
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
108        if (preg_match('/^\d+[KMG]?$/i', $value) !== 1) {
109            throw new RuntimeException(
110                self::MAX_MEMORY . ' must be a number of bytes, optionally with K, M or G, 0 for no limit',
114        return ini_parse_quantity($value);
115    }
FrankenPhpWorker->requestsSetting
87        $value = self::setting(self::MAX_REQUESTS);
88
89        if ($value === null) {
90            return null;
93        if (preg_match('/^\d+$/', $value) !== 1) {
93        if (preg_match('/^\d+$/', $value) !== 1) {
93        if (preg_match('/^\d+$/', $value) !== 1) {
93        if (preg_match('/^\d+$/', $value) !== 1) {
94            throw new RuntimeException(self::MAX_REQUESTS . ' must be a number of requests, 0 for no limit');
97        return (int) $value;
98    }
FrankenPhpWorker->retires
74    private function retires(App $app, int $handled): bool
75    {
76        return (
77            !$app->reusable()
78                || $this->maxRequests > 0
79                && $handled >= $this->maxRequests
79                && $handled >= $this->maxRequests
79                && $handled >= $this->maxRequests
80                || $this->maxMemory > 0
81                && memory_get_usage(true) >= $this->maxMemory
81                && memory_get_usage(true) >= $this->maxMemory
81                && memory_get_usage(true) >= $this->maxMemory
82        );
83    }
FrankenPhpWorker->serve
53    public function serve(App $app): false
54    {
55        // A client that disconnects must not abort a request halfway, as
56        // its teardown has to run before the worker takes the next one.
57        ignore_user_abort(true);
58        $handled = 0;
59
60        do {
61            $running = ($this->handleRequest)(static function () use ($app): void {
61            $running = ($this->handleRequest)(static function () use ($app): void {
62                // PHP's stat cache would keep file sizes and modification
63                // times from earlier requests.
64                clearstatcache();
65                $app->run();
66            });
67            $handled++;
68            gc_collect_cycles();
69        } while ($running && !$this->retires($app, $handled));
69        } while ($running && !$this->retires($app, $handled));
69        } while ($running && !$this->retires($app, $handled));
71        return false;
72    }
FrankenPhpWorker->setting
118    private static function setting(string $name): ?string
119    {
120        $value = $_SERVER[$name] ?? $_ENV[$name] ?? getenv($name);
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
121        $value = is_string($value) ? trim($value) : '';
122
123        return $value === '' ? null : $value;
123        return $value === '' ? null : $value;
123        return $value === '' ? null : $value;
123        return $value === '' ? null : $value;
124    }
{closure:/workspace/celema/core/src/Runtime/FrankenPhpWorker.php:61-66}
61            $running = ($this->handleRequest)(static function () use ($app): void {
62                // PHP's stat cache would keep file sizes and modification
63                // times from earlier requests.
64                clearstatcache();
65                $app->run();
66            });