diff --git a/README.md b/README.md index fe2df6db2903739864f5c2de721105e6caa36c0a..040583d9cc73edc2d0ebf8711dc18267934c5c52 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,14 @@ Pilot Settings**. There are several settings available: -* **Connection type:** This setting defines how the module connects to FastCGI. +* **Connection DSN:** This setting defines how the module connects to FastCGI. You have three options to choose from: - * **None:** This option disables the connection. It is useful for development - environments where you don't need this functionality. + * **Empty value:** This option disables the connection. It is useful for + development environments where you don't need this functionality. * **TCP:** Connects using the '[host]:[port]' information, for example: - `127.0.0.1:9000`. + `tcp://127.0.0.1:9000` or `tcp://php:9000`. * **Unix domain socket:** Connects using a unix socket to which you provide a - path, for example: `/var/run/php/php-fpm.sock`. + path, for example: `unix:///var/run/php/php-fpm.sock`. ## Usage @@ -62,7 +62,7 @@ To disable the functionality of the module in a particular environment, it is recommended to use settings.php to override the configuration: ```php -$config['cache_pilot.settings']['connection']['type'] = NULL; +$config['cache_pilot.settings']['connection_dsn'] = NULL; ``` [1]: https://www.drupal.org/docs/extending-drupal/installing-drupal-modules diff --git a/config/install/cache_pilot.settings.yml b/config/install/cache_pilot.settings.yml index a99e9e1d8311c484e4d84ef709827008e186dfe9..7b464d5109e50186e1df33b3a16e3749fd972cbc 100644 --- a/config/install/cache_pilot.settings.yml +++ b/config/install/cache_pilot.settings.yml @@ -1,4 +1 @@ -connection: - type: '' - host: '' - uds_path: '' +connection_dsn: '' \ No newline at end of file diff --git a/config/schema/cache_pilot.schema.yml b/config/schema/cache_pilot.schema.yml index 719e8247edc82b8847d1fd4cb46239ee1dbeea9d..5178d5a8c5d070bbaa46365ca7913ea8477b2f53 100644 --- a/config/schema/cache_pilot.schema.yml +++ b/config/schema/cache_pilot.schema.yml @@ -2,21 +2,6 @@ cache_pilot.settings: type: config_object label: Cache Pilot settings mapping: - connection: - type: mapping - label: Cache Pilot connection - mapping: - type: - type: string - label: Type - constraints: - Choice: - - '' - - 'tcp' - - 'uds' - host: - type: string - label: Host - uds_path: - type: string - label: Socket path + connection_dsn: + type: string + label: Cache Pilot DSN connection string \ No newline at end of file diff --git a/src/Connection/ConnectionConfig.php b/src/Connection/ConnectionConfig.php new file mode 100644 index 0000000000000000000000000000000000000000..484569ce3d3ead8671630f81beb85a6ecd4984c1 --- /dev/null +++ b/src/Connection/ConnectionConfig.php @@ -0,0 +1,152 @@ +<?php + +declare(strict_types=1); + +namespace Drupal\cache_pilot\Connection; + +/** + * Immutable configuration container for Fast-CGI connections. + */ +final readonly class ConnectionConfig { + + /** + * Constructs a new ConnectionConfig instance. + * + * @param non-empty-string $type + * The connection type. Supported values: + * - 'tcp': TCP/IP connection (requires $host and $port) + * - 'unix': Unix Domain Socket connection (requires $socketPath) + * @param non-empty-string|null $socketPath + * The filesystem path to the Unix socket. Must be: + * - Non-empty string for 'unix' type + * - NULL for 'tcp' type. + * @param string|null $host + * The TCP hostname/IP address. Must be: + * - Non-empty string for 'tcp' type + * - NULL for 'unix' type. + * @param int|null $port + * The TCP port number. Must be: + * - Valid port (1-65535) for 'tcp' type + * - NULL for 'unix' type. + */ + public function __construct( + public string $type, + public ?string $socketPath, + public ?string $host, + public ?int $port, + ) { + $this->validate(); + } + + /** + * Creates a ConnectionConfig instance from a DSN string. + * + * Supported DSN formats: + * - TCP: "tcp://host:port" + * - Unix: "unix:///path/to/socket.sock" + * + * @param string $dsn + * Data Source Name string. + * + * @return self + * Configured connection instance. + * + * @throws \InvalidArgumentException + * On invalid DSN format or parsing errors. + */ + public static function fromDsn(string $dsn): self { + [$type, $reminder] = \array_pad(\explode('://', $dsn), 2, NULL); + + if ($type === 'tcp') { + $connection = parse_url($reminder); + if (!$connection || !isset($connection['host']) || !isset($connection['port'])) { + throw new \InvalidArgumentException("Malformed DSN: {$dsn}."); + } + + return new self($type, NULL, $connection['host'], $connection['port']); + } + elseif ($type === 'unix') { + return new self($type, $reminder, NULL, NULL); + } + + throw new \InvalidArgumentException("Unsupported DSN type: {$type} (dsn: {$dsn})"); + } + + /** + * Performs comprehensive validation of all connection parameters. + * + * @throws \InvalidArgumentException + * When any validation rule is violated. + */ + private function validate(): void { + match ($this->type) { + 'tcp' => $this->validateTcpParams(), + 'unix' => $this->validateUnixParams(), + default => throw new \InvalidArgumentException("Invalid connection type '{$this->type}'. Allowed values: tcp, unix"), + }; + } + + /** + * Validates TCP-specific connection parameters. + * + * @throws \InvalidArgumentException + * When TCP parameters are invalid or inconsistent. + */ + private function validateTcpParams(): void { + $errors = []; + + if ($this->host === NULL || trim($this->host) === '') { + $errors[] = 'Host is required for TCP connections'; + } + + if ($this->port === NULL) { + $errors[] = 'Port is required for TCP connections'; + } + elseif ($this->port < 1 || $this->port > 65535) { + $errors[] = "Invalid port '{$this->port}'. Must be 1-65535"; + } + + if ($this->socketPath !== NULL) { + $errors[] = 'Socket path must be null for TCP connections'; + } + + $this->throwIfErrors($errors); + } + + /** + * Validates Unix socket-specific connection parameters. + * + * @throws \InvalidArgumentException + * When Unix parameters are invalid or inconsistent. + */ + private function validateUnixParams(): void { + $errors = []; + + if ($this->socketPath === NULL || trim($this->socketPath) === '') { + $errors[] = 'Socket path is required for Unix connections'; + } + + if ($this->host !== NULL || $this->port !== NULL) { + $errors[] = 'Host and port must be null for Unix connections'; + } + + $this->throwIfErrors($errors); + } + + /** + * Throws aggregated validation errors as a single exception. + * + * @param list<string> $errors + * List of validation error messages. + * + * @throws \InvalidArgumentException + */ + private function throwIfErrors(array $errors): void { + if (count($errors) === 0) { + return; + } + + throw new \InvalidArgumentException((implode('. ', $errors))); + } + +} diff --git a/src/Connection/SocketConnectionBuilder.php b/src/Connection/SocketConnectionBuilder.php index 4b7a642affb02ba9b7af10f8de347f270af61292..bbbb19f739c6b0524775f41ccf0e4ddc786eccfb 100644 --- a/src/Connection/SocketConnectionBuilder.php +++ b/src/Connection/SocketConnectionBuilder.php @@ -7,7 +7,6 @@ namespace Drupal\cache_pilot\Connection; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\cache_pilot\Exception\InvalidConfigurationException; use Drupal\cache_pilot\Exception\MissingConnectionTypeException; -use Drupal\cache_pilot\Utils\HostHelper; use hollodotme\FastCGI\Interfaces\ConfiguresSocketConnection; use hollodotme\FastCGI\SocketConnections\NetworkSocket; use hollodotme\FastCGI\SocketConnections\UnixDomainSocket; @@ -26,26 +25,27 @@ final readonly class SocketConnectionBuilder { * * @return \hollodotme\FastCGI\Interfaces\ConfiguresSocketConnection * The socket connection configuration. + * + * @throws \Drupal\cache_pilot\Exception\InvalidConfigurationException + * @throws \Drupal\cache_pilot\Exception\MissingConnectionTypeException */ public function build(): ConfiguresSocketConnection { - $settings = $this->configFactory->get('cache_pilot.settings'); - - if (!$settings->get('connection.type')) { + $connection_dsn = $this->configFactory->get('cache_pilot.settings')->get('connection_dsn'); + if (!$connection_dsn || !\is_string($connection_dsn)) { throw new MissingConnectionTypeException(); } - if ($settings->get('connection.type') === 'tcp') { - \assert(\is_string($settings->get('connection.host'))); - $host_info = HostHelper::parseHostInfo($settings->get('connection.host')); - return new NetworkSocket($host_info['host'], $host_info['port']); + $connection_config = ConnectionConfig::fromDsn($connection_dsn); + if ($connection_config->type === 'tcp') { + \assert(\is_string($connection_config->host) && \is_numeric($connection_config->port)); + return new NetworkSocket($connection_config->host, $connection_config->port); } - - if ($settings->get('connection.type') !== 'uds') { - \assert(\is_string($settings->get('connection.uds_path'))); - return new UnixDomainSocket($settings->get('connection.uds_path')); + if ($connection_config->type === 'unix') { + \assert(\is_string($connection_config->socketPath)); + return new UnixDomainSocket($connection_config->socketPath); } - throw new InvalidConfigurationException('The connection type is not supported.'); + throw new InvalidConfigurationException("Malformed DSN configuration: {$connection_dsn}"); } } diff --git a/src/Form/SettingsForm.php b/src/Form/SettingsForm.php index 012f56fc019de9810eee3475eb2810ade1f37261..a8f2a40ec54bbf22146f353d85bbb874e3818a63 100644 --- a/src/Form/SettingsForm.php +++ b/src/Form/SettingsForm.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Drupal\cache_pilot\Form; +use Drupal\cache_pilot\Connection\ConnectionConfig; use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Config\TypedConfigManagerInterface; use Drupal\Core\Form\ConfigFormBase; @@ -84,45 +85,12 @@ final class SettingsForm extends ConfigFormBase { '#description' => new TranslatableMarkup('Set up the connection to PHP, which will be used to interact with the cache.'), ]; - $form['connection']['connection_type'] = [ - '#type' => 'select', - '#title' => new TranslatableMarkup('Connection type'), - '#options' => [ - 'tcp' => new TranslatableMarkup('TCP'), - 'uds' => new TranslatableMarkup('Unix domain socket'), - ], - '#empty_option' => new TranslatableMarkup('None'), - '#default_value' => $this->config('cache_pilot.settings')->get('connection.type'), - ]; - - $form['connection']['connection_host'] = [ - '#type' => 'textfield', - '#title' => new TranslatableMarkup('Host'), - '#placeholder' => '127.0.0.1:9000', - '#states' => [ - 'visible' => [ - ':input[name="connection_type"]' => ['value' => 'tcp'], - ], - 'required' => [ - ':input[name="connection_type"]' => ['value' => 'tcp'], - ], - ], - '#default_value' => $this->config('cache_pilot.settings')->get('connection.host'), - ]; - - $form['connection']['connection_uds_path'] = [ + $form['connection']['connection_dsn'] = [ '#type' => 'textfield', - '#title' => new TranslatableMarkup('Socket path'), - '#placeholder' => '/var/run/php/php-fpm.sock', - '#states' => [ - 'visible' => [ - ':input[name="connection_type"]' => ['value' => 'uds'], - ], - 'required' => [ - ':input[name="connection_type"]' => ['value' => 'uds'], - ], - ], - '#default_value' => $this->config('cache_pilot.settings')->get('connection.uds_path'), + '#title' => new TranslatableMarkup('Connection DSN'), + '#description' => new TranslatableMarkup('The connection string in the format <code>tcp://[host]:[port]</code> for TCP or <code>unix://[socket_path]</code> for a Unix socket.'), + '#default_value' => $this->config('cache_pilot.settings')->get('connection_dsn'), + '#placeholder' => 'tcp://127.0.0.1:9000 or unix:///var/run/php/php-fpm.sock', ]; } @@ -185,20 +153,26 @@ final class SettingsForm extends ConfigFormBase { * {@inheritdoc} */ public function validateForm(array &$form, FormStateInterface $form_state): void { - // Not needed. + $connection_dsn = $form_state->getValue('connection_dsn'); + + if (!$connection_dsn) { + return; + } + + try { + \assert(is_string($connection_dsn)); + ConnectionConfig::fromDsn($connection_dsn); + } + catch (\InvalidArgumentException $exception) { + $form_state->setError($form['connection']['connection_dsn'], $exception->getMessage()); + } } /** * {@inheritdoc} */ public function submitForm(array &$form, FormStateInterface $form_state): void { - $this - ->config('cache_pilot.settings') - ->set('connection.type', $form_state->getValue('connection_type')) - ->set('connection.host', $form_state->getValue('connection_host')) - ->set('connection.port', $form_state->getValue('connection_port')) - ->set('connection.uds_path', $form_state->getValue('connection_uds_path')) - ->save(); + $this->config('cache_pilot.settings')->set('connection_dsn', $form_state->getValue('connection_dsn'))->save(); } /** diff --git a/src/Utils/HostHelper.php b/src/Utils/HostHelper.php deleted file mode 100644 index 271ca80b8a20a92b417218bd71e9c2418c766518..0000000000000000000000000000000000000000 --- a/src/Utils/HostHelper.php +++ /dev/null @@ -1,37 +0,0 @@ -<?php - -declare(strict_types=1); - -namespace Drupal\cache_pilot\Utils; - -/** - * Provides helpers for host values. - */ -final readonly class HostHelper { - - /** - * Parses host information. - * - * @param string $host - * The host to parse, port should be separated by colon. - * - * @return array{ - * 'host': string, - * 'port': int, - * } - */ - public static function parseHostInfo(string $host): array { - $info = parse_url($host); - - if (!isset($info['host']) || !isset($info['port'])) { - $message = sprintf( - "The provided host should be in format '[address]:[port]', %s provided", - $host, - ); - throw new \InvalidArgumentException($message); - } - - return $info; - } - -}