diff --git a/src/Connection.php b/src/Connection.php index 1cd009f..3153ee5 100644 --- a/src/Connection.php +++ b/src/Connection.php @@ -28,6 +28,8 @@ class Connection extends AbstractConnection implements DriverAwareInterface { protected Driver $driver; + protected ?MysqliConnectionParameters $params = null; + /** @var mysqli */ protected $resource; @@ -37,48 +39,42 @@ class Connection extends AbstractConnection implements DriverAwareInterface * @throws InvalidArgumentException */ public function __construct( - array|mysqli|null $connectionInfo = null + array|mysqli $connectionInfo, ) { if (is_array($connectionInfo)) { $this->setConnectionParameters($connectionInfo); - } elseif ($connectionInfo instanceof mysqli) { + } + + if ($connectionInfo instanceof mysqli) { $this->setResource($connectionInfo); - } elseif (null !== $connectionInfo) { - throw new Exception\InvalidArgumentException( - '$connection must be an array of parameters, a mysqli object or null' - ); } } - public function setDriver(DriverInterface $driver): DriverAwareInterface + /** @inheritDoc */ + #[Override] + public function beginTransaction(): ConnectionInterface { - $this->driver = $driver; + if (! $this->isConnected()) { + $this->connect(); + } + + $this->resource->autocommit(false); + $this->inTransaction = true; return $this; } /** @inheritDoc */ #[Override] - public function getCurrentSchema(): string|false + public function commit(): ConnectionInterface { if (! $this->isConnected()) { $this->connect(); } - $result = $this->resource->query('SELECT DATABASE()'); - $r = $result->fetch_row(); - - return $r[0]; - } - - /** - * Set resource - * - * @return $this Provides a fluent interface - */ - public function setResource(mysqli $resource): static - { - $this->resource = $resource; + $this->resource->commit(); + $this->inTransaction = false; + $this->resource->autocommit(true); return $this; } @@ -91,104 +87,25 @@ public function connect(): ConnectionInterface return $this; } - /** @var array $p */ - $p = $this->connectionParameters; - - // given a list of key names, test for existence in $p - /** @var string[] $names */ - $findParameterValue = function (array $names) use ($p): string|null { - foreach ($names as $name) { - if (isset($p[$name])) { - return $p[$name]; - } - } - - return null; - }; - - /** @var string|null $hostname */ - $hostname = $findParameterValue(['hostname', 'host']); - $username = $findParameterValue(['username', 'user']); - $password = $findParameterValue(['password', 'passwd', 'pw']); - $database = $findParameterValue(['database', 'dbname', 'db', 'schema']); - /** @var int|null $port */ - $port = isset($p['port']) ? (int) $p['port'] : null; - /** @var string|null $socket */ - $socket = $p['socket'] ?? null; - - // phpcs:ignore WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps - $useSSL = $p['use_ssl'] ?? 0; - $clientKey = $p['client_key'] ?? ''; - $clientCert = $p['client_cert'] ?? ''; - $caCert = $p['ca_cert'] ?? ''; - $caPath = $p['ca_path'] ?? ''; - $cipher = $p['cipher'] ?? ''; + $params = $this->params ??= MysqliConnectionParameters::fromArray($this->connectionParameters); $this->resource = $this->createResource(); - if (! empty($p['driver_options'])) { - foreach ($p['driver_options'] as $option => $value) { - if (is_string($option)) { - $option = strtoupper($option); - if (! defined($option)) { - continue; - } - $option = constant($option); - } - $this->resource->options($option, $value); - } + if ([] !== $params->driverOptions) { + $this->applyDriverOptions($this->resource, $params->driverOptions); } - $flags = null; - - // phpcs:ignore WebimpressCodingStandard.NamingConventions.ValidVariableName.NotCamelCaps - if ($useSSL && ! $socket) { - // Even though mysqli docs are not quite clear on this, MYSQLI_CLIENT_SSL - // needs to be set to make sure SSL is used. ssl_set can also cause it to - // be implicitly set, but only when any of the parameters is non-empty. - $flags = MYSQLI_CLIENT_SSL; - $this->resource->ssl_set($clientKey, $clientCert, $caCert, $caPath, $cipher); - //MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT is not valid option, needs to be set as flag - if ( - isset($p['driver_options'][MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT]) - ) { - $flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; - } - } + $flags = $this->applySsl($this->resource, $params); - try { - $flags === null - ? $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket) - : $this->resource->real_connect($hostname, $username, $password, $database, $port, $socket, $flags); - } catch (GenericException) { - throw new Exception\RuntimeException( - 'Connection error', - $this->resource->connect_errno, - new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) - ); - } - - if ($this->resource->connect_error) { - throw new Exception\RuntimeException( - 'Connection error', - $this->resource->connect_errno, - new Exception\ErrorException($this->resource->connect_error, $this->resource->connect_errno) - ); - } + $this->performRealConnect($this->resource, $params, $flags); - if (! empty($p['charset'])) { - $this->resource->set_charset($p['charset']); + if (! empty($params->charset)) { + $this->resource->set_charset($params->charset); } return $this; } - /** @inheritDoc */ - public function isConnected(): bool - { - return $this->resource instanceof mysqli; - } - /** @inheritDoc */ #[Override] public function disconnect(): ConnectionInterface @@ -200,33 +117,57 @@ public function disconnect(): ConnectionInterface return $this; } - /** @inheritDoc */ + /** + * {@inheritDoc} + * + * @throws Exception\InvalidQueryException + */ #[Override] - public function beginTransaction(): ConnectionInterface + public function execute(string $sql): ?ResultInterface { if (! $this->isConnected()) { $this->connect(); } - $this->resource->autocommit(false); - $this->inTransaction = true; + $this->profiler?->profilerStart($sql); - return $this; + $resultResource = $this->resource->query($sql); + + $this->profiler?->profilerFinish(); + + // if the returnValue is something other than a mysqli_result, bypass wrapping it + if (false === $resultResource) { + throw new Exception\InvalidQueryException($this->resource->error); + } + + return $this->driver->createResult($resultResource === true ? $this->resource : $resultResource); } /** @inheritDoc */ #[Override] - public function commit(): ConnectionInterface + public function getCurrentSchema(): string|false { if (! $this->isConnected()) { $this->connect(); } - $this->resource->commit(); - $this->inTransaction = false; - $this->resource->autocommit(true); + $result = $this->resource->query('SELECT DATABASE()'); + $r = $result->fetch_row(); - return $this; + return $r[0]; + } + + /** @inheritDoc */ + #[Override] + public function getLastGeneratedValue(?string $name = null): string|int|false|null + { + return $this->resource->insert_id; + } + + /** @inheritDoc */ + public function isConnected(): bool + { + return $this->resource instanceof mysqli; } /** @inheritDoc */ @@ -249,36 +190,38 @@ public function rollback(): ConnectionInterface } /** - * {@inheritDoc} + * Set connection parameters * - * @throws Exception\InvalidQueryException + * Also normalizes the raw array into a {@see MysqliConnectionParameters} value + * object, cached for the lifetime of the connection so that {@see connect()} + * does not need to re-parse the raw array on every call. */ #[Override] - public function execute($sql): ?ResultInterface + public function setConnectionParameters(array $connectionParameters): ConnectionInterface { - if (! $this->isConnected()) { - $this->connect(); - } + parent::setConnectionParameters($connectionParameters); + $this->params = MysqliConnectionParameters::fromArray($connectionParameters); - $this->profiler?->profilerStart($sql); - - $resultResource = $this->resource->query($sql); - - $this->profiler?->profilerFinish($sql); + return $this; + } - // if the returnValue is something other than a mysqli_result, bypass wrapping it - if ($resultResource === false) { - throw new Exception\InvalidQueryException($this->resource->error); - } + public function setDriver(DriverInterface $driver): DriverAwareInterface + { + $this->driver = $driver; - return $this->driver->createResult($resultResource === true ? $this->resource : $resultResource); + return $this; } - /** @inheritDoc */ - #[Override] - public function getLastGeneratedValue(?string $name = null): string|int|false|null + /** + * Set resource + * + * @return $this Provides a fluent interface + */ + public function setResource(mysqli $resource): static { - return $this->resource->insert_id; + $this->resource = $resource; + + return $this; } /** @@ -292,4 +235,98 @@ protected function createResource() { return new mysqli(); } + + /** + * Apply mysqli `driver_options` to the resource prior to connecting. + * + * String keys are resolved against defined `MYSQLI_*` constants (case-insensitively); + * unresolvable string keys are ignored. Integer keys (e.g. raw `MYSQLI_*` constants) + * are passed through unchanged. + * + * @param array $driverOptions + */ + private function applyDriverOptions(mysqli $resource, array $driverOptions): void + { + foreach ($driverOptions as $option => $value) { + if (is_string($option)) { + $option = strtoupper($option); + if (! defined($option)) { + continue; + } + $option = constant($option); + } + $resource->options($option, $value); + } + } + + /** + * Configure SSL on the resource, if requested, and return the connect flags to use. + * + * Even though mysqli docs are not quite clear on this, MYSQLI_CLIENT_SSL needs to be + * set to make sure SSL is used. ssl_set can also cause it to be implicitly set, but + * only when any of the parameters is non-empty. + */ + private function applySsl(mysqli $resource, MysqliConnectionParameters $params): ?int + { + if (! $params->ssl->enabled || $params->socket) { + return null; + } + + $flags = MYSQLI_CLIENT_SSL; + $resource->ssl_set( + $params->ssl->key, + $params->ssl->cert, + $params->ssl->caCert, + $params->ssl->caPath, + $params->ssl->cipher, + ); + + // MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT is not valid option, needs to be set as flag + if (isset($params->driverOptions[MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT])) { + $flags |= MYSQLI_CLIENT_SSL_DONT_VERIFY_SERVER_CERT; + } + + return $flags; + } + + /** + * @throws Exception\RuntimeException + */ + private function performRealConnect(mysqli $resource, MysqliConnectionParameters $params, ?int $flags): void + { + try { + null === $flags + ? $resource->real_connect( + $params->hostname, + $params->username, + $params->password, + $params->database, + $params->port, + $params->socket, + ) + : $resource->real_connect( + $params->hostname, + $params->username, + $params->password, + $params->database, + $params->port, + $params->socket, + $flags, + ); + } catch (GenericException) { + throw new Exception\RuntimeException( + 'Connection error', + $resource->connect_errno, + new Exception\ErrorException($resource->connect_error, $resource->connect_errno), + ); + } + + if ($resource->connect_error) { + throw new Exception\RuntimeException( + 'Connection error', + $resource->connect_errno, + new Exception\ErrorException($resource->connect_error, $resource->connect_errno), + ); + } + } } diff --git a/src/MysqliConnectionParameters.php b/src/MysqliConnectionParameters.php new file mode 100644 index 0000000..81d1a10 --- /dev/null +++ b/src/MysqliConnectionParameters.php @@ -0,0 +1,70 @@ + $driverOptions + */ + public function __construct( + public ?string $hostname = null, + public ?string $username = null, + public ?string $password = null, + public ?string $database = null, + public ?int $port = null, + public ?string $socket = null, + public ?string $charset = null, + public array $driverOptions = [], + public MysqliSslOptions $ssl = new MysqliSslOptions(), + ) {} + + /** + * @param array $params + */ + public static function fromArray(array $params): self + { + $hostname = $username = $password = $database = $port = $socket = $charset = null; + $driverOptions = []; + + foreach ($params as $key => $value) { + $result = match (strtolower((string) $key)) { + 'hostname', 'host' => $hostname = (string) $value, + 'username', 'user' => $username = (string) $value, + 'password', 'passwd', 'pw' => $password = (string) $value, + 'database', 'dbname', 'db', 'schema' => $database = (string) $value, + 'port' => $port = (int) $value, + 'socket' => $socket = (string) $value, + 'charset' => $charset = (string) $value, + 'driver_options' => $driverOptions = is_array($value) ? $value : [], + default => null, + }; + } + unset($result); + + return new self( + hostname : $hostname, + username : $username, + password : $password, + database : $database, + port : $port, + socket : $socket, + charset : $charset, + driverOptions: $driverOptions, + ssl : MysqliSslOptions::fromArray($params), + ); + } +} diff --git a/src/MysqliSslOptions.php b/src/MysqliSslOptions.php new file mode 100644 index 0000000..1078906 --- /dev/null +++ b/src/MysqliSslOptions.php @@ -0,0 +1,38 @@ + $params Raw connection parameters, as accepted by + * {@see \PhpDb\Mysql\Connection::setConnectionParameters()}. + */ + public static function fromArray(array $params): self + { + return new self( + enabled: (bool) ($params['use_ssl'] ?? false), + key: (string) ($params['client_key'] ?? ''), + cert: (string) ($params['client_cert'] ?? ''), + caCert: (string) ($params['ca_cert'] ?? ''), + caPath: (string) ($params['ca_path'] ?? ''), + cipher: (string) ($params['cipher'] ?? ''), + ); + } +}