pixelfederation/swoole-bundle

View on GitHub
src/Server/HttpServerConfiguration.php

Summary

Maintainability
C
1 day
Test Coverage
<?php

declare(strict_types=1);

namespace K911\Swoole\Server;

use Assert\Assertion;
use K911\Swoole\Server\Config\Socket;
use K911\Swoole\Server\Config\Sockets;

/**
 * @todo Create interface and split this class
 *
 * @final
 */
class HttpServerConfiguration
{
    private const SWOOLE_HTTP_SERVER_CONFIG_DAEMONIZE = 'daemonize';
    private const SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC = 'serve_static';
    private const SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT = 'reactor_count';
    private const SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT = 'worker_count';
    private const SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT = 'task_worker_count';
    private const SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR = 'public_dir';
    private const SWOOLE_HTTP_SERVER_CONFIG_LOG_FILE = 'log_file';
    private const SWOOLE_HTTP_SERVER_CONFIG_LOG_LEVEL = 'log_level';
    private const SWOOLE_HTTP_SERVER_CONFIG_PID_FILE = 'pid_file';
    private const SWOOLE_HTTP_SERVER_CONFIG_BUFFER_OUTPUT_SIZE = 'buffer_output_size';
    private const SWOOLE_HTTP_SERVER_CONFIG_PACKAGE_MAX_LENGTH = 'package_max_length';
    private const SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST = 'worker_max_request';
    private const SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST_GRACE = 'worker_max_request_grace';
    private const SWOOLE_HTTP_SERVER_CONFIG_ENABLE_COROUTINE = 'enable_coroutine';
    private const SWOOLE_HTTP_SERVER_CONFIG_MAX_COROUTINE = 'max_coroutine';
    private const SWOOLE_HTTP_SERVER_CONFIG_TASK_ENABLE_COROUTINE = 'task_enable_coroutine';
    private const SWOOLE_HTTP_SERVER_CONFIG_TASK_USE_OBJECT = 'task_use_object';
    private const SWOOLE_HTTP_SERVER_CONFIG_COROUTINE_HOOK_FLAGS = 'hook_flags';

    /**
     * @todo add more
     *
     * @see https://github.com/swoole/swoole-docs/blob/master/modules/swoole-server/configuration.md
     * @see https://github.com/swoole/swoole-docs/blob/master/modules/swoole-http-server/configuration.md
     */
    private const SWOOLE_HTTP_SERVER_CONFIGURATION = [
        self::SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT => 'reactor_num',
        self::SWOOLE_HTTP_SERVER_CONFIG_DAEMONIZE => 'daemonize',
        self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT => 'worker_num',
        self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC => 'enable_static_handler',
        self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR => 'document_root',
        self::SWOOLE_HTTP_SERVER_CONFIG_LOG_FILE => 'log_file',
        self::SWOOLE_HTTP_SERVER_CONFIG_LOG_LEVEL => 'log_level',
        self::SWOOLE_HTTP_SERVER_CONFIG_PID_FILE => 'pid_file',
        self::SWOOLE_HTTP_SERVER_CONFIG_BUFFER_OUTPUT_SIZE => 'buffer_output_size',
        self::SWOOLE_HTTP_SERVER_CONFIG_PACKAGE_MAX_LENGTH => 'package_max_length',
        self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT => 'task_worker_num',
        self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST => 'max_request',
        self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST_GRACE => 'max_request_grace',
        self::SWOOLE_HTTP_SERVER_CONFIG_ENABLE_COROUTINE => 'enable_coroutine',
        self::SWOOLE_HTTP_SERVER_CONFIG_TASK_ENABLE_COROUTINE => 'task_enable_coroutine',
        self::SWOOLE_HTTP_SERVER_CONFIG_MAX_COROUTINE => 'max_coroutine',
        self::SWOOLE_HTTP_SERVER_CONFIG_TASK_USE_OBJECT => 'task_use_object',
        self::SWOOLE_HTTP_SERVER_CONFIG_COROUTINE_HOOK_FLAGS => 'hook_flags',
    ];

    private const SWOOLE_SERVE_STATIC = [
        'off' => false,
        'advanced' => false,
        'default' => true,
    ];

    private const SWOOLE_LOG_LEVELS = [
        'debug' => \SWOOLE_LOG_DEBUG,
        'trace' => \SWOOLE_LOG_TRACE,
        'info' => \SWOOLE_LOG_INFO,
        'notice' => \SWOOLE_LOG_NOTICE,
        'warning' => \SWOOLE_LOG_WARNING,
        'error' => \SWOOLE_LOG_ERROR,
    ];

    /**
     * @var array<string, mixed>
     */
    private array $settings;

    /**
     * @param array $settings settings available:
     *                        - reactor_count (default: number of cpu cores)
     *                        - worker_count (default: 2 * number of cpu cores)
     *                        - task_worker_count (default: unset; "auto" => number of cpu cores; number of task workers)
     *                        - serve_static_files (default: false)
     *                        - public_dir (default: '%kernel.root_dir%/public')
     *                        - buffer_output_size (default: '2097152' unit in byte (2MB))
     *                        - package_max_length (default: '8388608b' unit in byte (8MB))
     *                        - worker_max_requests: Number of requests after which the worker reloads
     *                        - worker_max_requests_grace: Max random number of requests for worker reloading
     *                        - enable_coroutine: enable coroutines in web processes
     *                        - task_enable_coroutine: enable coroutines in task workers
     *                        - task_use_object: enable OOP style task API
     *                        - hook_flags: coroutine hook flags
     *
     * @throws \Assert\AssertionFailedException
     */
    public function __construct(
        private Sockets $sockets,
        private string $runningMode = 'process',
        array $settings = [],
        private ?int $maxConcurrency = null
    ) {
        $this->changeRunningMode($runningMode);
        $this->initializeSettings($settings);
    }

    public function isDaemon(): bool
    {
        return isset($this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_DAEMONIZE]);
    }

    public function hasPidFile(): bool
    {
        return isset($this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_PID_FILE]);
    }

    public function servingStaticContent(): bool
    {
        return isset($this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC]) && 'off' !== $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC];
    }

    public function hasPublicDir(): bool
    {
        return !empty($this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR]);
    }

    public function changeServerSocket(Socket $socket): void
    {
        $this->sockets->changeServerSocket($socket);
    }

    public function getSockets(): Sockets
    {
        return $this->sockets;
    }

    public function getMaxConcurrency(): ?int
    {
        return $this->maxConcurrency;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public function enableServingStaticFiles(string $publicDir): void
    {
        $settings = [
            self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR => $publicDir,
        ];

        if ('off' === $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC]) {
            $settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC] = 'default';
        }

        $this->setSettings($settings);
    }

    public function isReactorRunningMode(): bool
    {
        return 'reactor' === $this->runningMode;
    }

    public function getRunningMode(): string
    {
        return $this->runningMode;
    }

    public function getCoroutinesEnabled(): bool
    {
        return isset($this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_ENABLE_COROUTINE])
            && $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_ENABLE_COROUTINE];
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public function getPid(): int
    {
        Assertion::true($this->existsPidFile(), 'Could not get pid file. It does not exists or server is not running in background.');

        /** @var string $contents */
        $contents = file_get_contents($this->getPidFile());
        Assertion::numeric($contents, 'Contents in pid file is not an integer or it is empty');

        return (int) $contents;
    }

    public function existsPidFile(): bool
    {
        return $this->hasPidFile() && file_exists($this->getPidFile());
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public function getPidFile(): string
    {
        Assertion::keyIsset($this->settings, self::SWOOLE_HTTP_SERVER_CONFIG_PID_FILE, 'Setting "%s" is not set.');

        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_PID_FILE];
    }

    public function getWorkerCount(): int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT];
    }

    public function getReactorCount(): int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT];
    }

    public function getServerSocket(): Socket
    {
        return $this->sockets->getServerSocket();
    }

    public function getMaxRequest(): int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST];
    }

    public function getMaxRequestGrace(): ?int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST_GRACE] ?? null;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public function getPublicDir(): string
    {
        Assertion::true($this->hasPublicDir(), sprintf('Setting "%s" is not set or empty.', self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR));

        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR];
    }

    public function getSettings(): array
    {
        return $this->settings;
    }

    /**
     * Get settings formatted for swoole http server.
     *
     * @see \Swoole\Http\Server::set()
     *
     * @todo create swoole settings transformer
     */
    public function getSwooleSettings(): array
    {
        $swooleSettings = [];
        foreach ($this->settings as $key => $setting) {
            $swooleSettingKey = self::SWOOLE_HTTP_SERVER_CONFIGURATION[$key];
            $swooleGetter = sprintf('getSwoole%s', str_replace('_', '', $swooleSettingKey));
            if (method_exists($this, $swooleGetter)) {
                $setting = $this->{$swooleGetter}();
            }

            if (null !== $setting) {
                $swooleSettings[$swooleSettingKey] = $setting;
            }
        }

        return $swooleSettings;
    }

    /**
     * @see getSwooleSettings()
     */
    public function getSwooleLogLevel(): int
    {
        return self::SWOOLE_LOG_LEVELS[$this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_LOG_LEVEL]];
    }

    /**
     * @see getSwooleSettings()
     */
    public function getSwooleEnableStaticHandler(): bool
    {
        return self::SWOOLE_SERVE_STATIC[$this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC]];
    }

    /**
     * @see getSwooleSettings()
     */
    public function getSwooleDocumentRoot(): ?string
    {
        return 'default' === $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC] ? $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR] : null;
    }

    /**
     * @see getSwooleSettings()
     */
    public function getSwooleMaxRequest(): int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST] ?? 0;
    }

    /**
     * @see getSwooleSettings()
     */
    public function getSwooleMaxRequestGrace(): ?int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST_GRACE] ?? null;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    public function daemonize(?string $pidFile = null): void
    {
        $settings = [self::SWOOLE_HTTP_SERVER_CONFIG_DAEMONIZE => true];

        if (null !== $pidFile) {
            $settings[self::SWOOLE_HTTP_SERVER_CONFIG_PID_FILE] = $pidFile;
        }

        $this->setSettings($settings);
    }

    public function getTaskWorkerCount(): int
    {
        return $this->settings[self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT] ?? 0;
    }

    private function changeRunningMode(string $runningMode): void
    {
        Assertion::inArray($runningMode, ['process', 'reactor', 'thread']);

        $this->runningMode = $runningMode;
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    private function initializeSettings(array $init): void
    {
        $this->settings = [];
        $cpuCores = swoole_cpu_num();

        if (!isset($init[self::SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT])) {
            $init[self::SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT] = $cpuCores;
        }

        if (!isset($init[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT])) {
            $init[self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT] = 2 * $cpuCores;
        }

        if (\array_key_exists(self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT, $init) && 'auto' === $init[self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT]) {
            $init[self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT] = $cpuCores;
        }

        $this->setSettings($init);
    }

    /**
     * @param array<string, mixed> $settings
     *
     * @throws \Assert\AssertionFailedException
     */
    private function setSettings(array $settings): void
    {
        foreach ($settings as $name => $value) {
            if (null !== $value) {
                $this->validateSetting($name, $value);
                $this->settings[$name] = $value;
            }
        }

        Assertion::false($this->isDaemon() && !$this->hasPidFile(), 'Pid file is required when using daemon mode');
        Assertion::false($this->servingStaticContent() && !$this->hasPublicDir(), 'Enabling static files serving requires providing "public_dir" setting.');
    }

    /**
     * @throws \Assert\AssertionFailedException
     */
    private function validateSetting(string $key, mixed $value): void
    {
        Assertion::keyExists(self::SWOOLE_HTTP_SERVER_CONFIGURATION, $key, 'There is no configuration mapping for setting "%s".');

        switch ($key) {
            case self::SWOOLE_HTTP_SERVER_CONFIG_SERVE_STATIC:
                Assertion::inArray($value, array_keys(self::SWOOLE_SERVE_STATIC));

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_DAEMONIZE:
            case self::SWOOLE_HTTP_SERVER_CONFIG_TASK_USE_OBJECT:
                Assertion::boolean($value);

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_PUBLIC_DIR:
                Assertion::directory($value, 'Public directory does not exists. Tried "%s".');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_LOG_LEVEL:
                Assertion::inArray($value, array_keys(self::SWOOLE_LOG_LEVELS));

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_PACKAGE_MAX_LENGTH:
                Assertion::integer($value, sprintf('Setting "%s" must be an integer.', $key));
                Assertion::greaterThan($value, 0, 'Package max length value cannot be negative or zero, "%s" provided.');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_BUFFER_OUTPUT_SIZE:
                Assertion::integer($value, sprintf('Setting "%s" must be an integer.', $key));
                Assertion::greaterThan($value, 0, 'Buffer output size value cannot be negative or zero, "%s" provided.');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_TASK_WORKER_COUNT:
            case self::SWOOLE_HTTP_SERVER_CONFIG_REACTOR_COUNT:
            case self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_COUNT:
                Assertion::integer($value, sprintf('Setting "%s" must be an integer.', $key));
                Assertion::greaterThan($value, 0, 'Count value cannot be negative, "%s" provided.');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST:
                Assertion::integer($value, sprintf('Setting "%s" must be an integer.', $key));
                Assertion::greaterOrEqualThan($value, 0, 'Value cannot be negative, "%s" provided.');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_WORKER_MAX_REQUEST_GRACE:
                Assertion::nullOrInteger($value, sprintf('Setting "%s" must be an integer or null.', $key));
                Assertion::nullOrGreaterOrEqualThan($value, 0, 'Value cannot be negative, "%s" provided.');

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_ENABLE_COROUTINE:
            case self::SWOOLE_HTTP_SERVER_CONFIG_TASK_ENABLE_COROUTINE:
                Assertion::boolean($value, sprintf('Setting "%s" must be a boolean.', $key));

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_COROUTINE_HOOK_FLAGS:
                Assertion::integer($value, sprintf('Setting "%s" must be a positive integer.', $key));
                Assertion::greaterOrEqualThan($value, 0, sprintf('Setting "%s" must be a positive integer.', $key));

                break;
            case self::SWOOLE_HTTP_SERVER_CONFIG_MAX_COROUTINE:
                Assertion::integer(
                    $value,
                    sprintf('Setting "%s" must be a positive integer lower or equal than 100000.', $key)
                );
                Assertion::between(
                    $value,
                    0,
                    100000,
                    sprintf('Setting "%s" must be a positive integer lower or equal than 100000.', $key)
                );
                // no break
            default:
                return;
        }
    }
}