diff --git a/packages/database/src/Config/DatabaseConfig.php b/packages/database/src/Config/DatabaseConfig.php index 0635d2f68..a341b257d 100644 --- a/packages/database/src/Config/DatabaseConfig.php +++ b/packages/database/src/Config/DatabaseConfig.php @@ -9,23 +9,52 @@ interface DatabaseConfig extends HasTag { + /** + * PDO data source name connection string. + */ public string $dsn { get; } + /** + * The naming strategy for database tables and columns. + */ public NamingStrategy $namingStrategy { get; } + /** + * The database dialect (MySQL, PostgreSQL, SQLite). + */ public DatabaseDialect $dialect { get; } + /** + * The database username for authentication. + */ public ?string $username { get; } + /** + * The database password for authentication. + */ public ?string $password { get; } + + /** + * Whether to use persistent database connections. + */ + public bool $usePersistentConnection { + get; + } + + /** + * PDO connection options built from configuration properties. + */ + public array $options { + get; + } } diff --git a/packages/database/src/Config/MysqlConfig.php b/packages/database/src/Config/MysqlConfig.php index cdde854f6..56c8e853d 100644 --- a/packages/database/src/Config/MysqlConfig.php +++ b/packages/database/src/Config/MysqlConfig.php @@ -4,6 +4,8 @@ namespace Tempest\Database\Config; +use PDO; +use Pdo\Mysql; use SensitiveParameter; use Tempest\Database\Tables\NamingStrategy; use Tempest\Database\Tables\PluralizedSnakeCaseStrategy; @@ -24,6 +26,52 @@ final class MysqlConfig implements DatabaseConfig get => DatabaseDialect::MYSQL; } + public bool $usePersistentConnection { + get => $this->persistent; + } + + public array $options { + get { + $options = []; + + if ($this->persistent) { + $options[PDO::ATTR_PERSISTENT] = true; + } + + if ($this->certificateAuthority !== null) { + $options[Mysql::ATTR_SSL_CA] = $this->certificateAuthority; + } + + if ($this->verifyServerCertificate !== null) { + $options[Mysql::ATTR_SSL_VERIFY_SERVER_CERT] = $this->verifyServerCertificate; + } + + if ($this->clientCertificate !== null) { + $options[Mysql::ATTR_SSL_CERT] = $this->clientCertificate; + } + + if ($this->clientKey !== null) { + $options[Mysql::ATTR_SSL_KEY] = $this->clientKey; + } + + return $options; + } + } + + /** + * @param string $host The MySQL server hostname or IP address. + * @param string $port The MySQL server port number. + * @param string $username The MySQL username for authentication. + * @param string $password The MySQL password for authentication. + * @param string $database The database name to connect to. + * @param bool $persistent Whether to use persistent connections. Persistent connections are not closed at the end of the script and are cached for reuse when another script requests a connection using the same credentials. + * @param bool|null $verifyServerCertificate Whether to verify the server's SSL certificate. Set to false for self-signed certificates (not recommended for production). + * @param string|null $certificateAuthority Path to the SSL Certificate Authority (CA) file. Required for SSL/TLS connections to verify the server's certificate. + * @param string|null $clientCertificate Path to the client's SSL certificate file. Used for mutual TLS authentication. + * @param string|null $clientKey Path to the client's SSL private key file. Used for mutual TLS authentication. + * @param NamingStrategy $namingStrategy The naming strategy for database tables and columns. + * @param string|UnitEnum|null $tag An optional tag to identify this database configuration. + */ public function __construct( #[SensitiveParameter] public string $host = 'localhost', @@ -35,6 +83,11 @@ public function __construct( public string $password = '', #[SensitiveParameter] public string $database = 'app', + public bool $persistent = false, + public ?bool $verifyServerCertificate = null, + public ?string $certificateAuthority = null, + public ?string $clientCertificate = null, + public ?string $clientKey = null, public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), public null|string|UnitEnum $tag = null, ) {} diff --git a/packages/database/src/Config/PostgresConfig.php b/packages/database/src/Config/PostgresConfig.php index 13b447f4c..991056144 100644 --- a/packages/database/src/Config/PostgresConfig.php +++ b/packages/database/src/Config/PostgresConfig.php @@ -4,6 +4,7 @@ namespace Tempest\Database\Config; +use PDO; use SensitiveParameter; use Tempest\Database\Tables\NamingStrategy; use Tempest\Database\Tables\PluralizedSnakeCaseStrategy; @@ -26,6 +27,32 @@ final class PostgresConfig implements DatabaseConfig get => DatabaseDialect::POSTGRESQL; } + public bool $usePersistentConnection { + get => $this->persistent; + } + + public array $options { + get { + $options = []; + + if ($this->persistent) { + $options[PDO::ATTR_PERSISTENT] = true; + } + + return $options; + } + } + + /** + * @param string $host The PostgreSQL server hostname or IP address. + * @param string $port The PostgreSQL server port number. + * @param string $username The PostgreSQL username for authentication. + * @param string $password The PostgreSQL password for authentication. + * @param string $database The database name to connect to. + * @param bool $persistent Whether to use persistent connections. Persistent connections are not closed at the end of the script and are cached for reuse when another script requests a connection using the same credentials. + * @param NamingStrategy $namingStrategy The naming strategy for database tables and columns. + * @param string|UnitEnum|null $tag An optional tag to identify this database configuration. + */ public function __construct( #[SensitiveParameter] public string $host = '127.0.0.1', @@ -37,6 +64,7 @@ public function __construct( public string $password = '', #[SensitiveParameter] public string $database = 'app', + public bool $persistent = false, public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), public null|string|UnitEnum $tag = null, ) {} diff --git a/packages/database/src/Config/SQLiteConfig.php b/packages/database/src/Config/SQLiteConfig.php index 155373b2c..d161a674b 100644 --- a/packages/database/src/Config/SQLiteConfig.php +++ b/packages/database/src/Config/SQLiteConfig.php @@ -4,6 +4,7 @@ namespace Tempest\Database\Config; +use PDO; use SensitiveParameter; use Tempest\Database\Tables\NamingStrategy; use Tempest\Database\Tables\PluralizedSnakeCaseStrategy; @@ -30,9 +31,32 @@ final class SQLiteConfig implements DatabaseConfig get => DatabaseDialect::SQLITE; } + public bool $usePersistentConnection { + get => $this->persistent; + } + + public array $options { + get { + $options = []; + + if ($this->persistent) { + $options[PDO::ATTR_PERSISTENT] = true; + } + + return $options; + } + } + + /** + * @param string $path Path to the SQLite database file. Use ':memory:' for an in-memory database. + * @param bool $persistent Whether to use persistent connections. Persistent connections are not closed at the end of the script and are cached for reuse when another script requests a connection using the same credentials. + * @param NamingStrategy $namingStrategy The naming strategy for database tables and columns. + * @param string|UnitEnum|null $tag An optional tag to identify this database configuration. + */ public function __construct( #[SensitiveParameter] public string $path = 'localhost', + public bool $persistent = false, public NamingStrategy $namingStrategy = new PluralizedSnakeCaseStrategy(), public null|string|UnitEnum $tag = null, ) {} diff --git a/packages/database/src/Connection/PDOConnection.php b/packages/database/src/Connection/PDOConnection.php index a4b9bf696..1b3e6ae98 100644 --- a/packages/database/src/Connection/PDOConnection.php +++ b/packages/database/src/Connection/PDOConnection.php @@ -102,6 +102,7 @@ public function connect(): void dsn: $this->config->dsn, username: $this->config->username, password: $this->config->password, + options: $this->config->options, ); } } diff --git a/packages/database/tests/Config/DatabaseConfigTest.php b/packages/database/tests/Config/DatabaseConfigTest.php index 04951017b..02df0a722 100644 --- a/packages/database/tests/Config/DatabaseConfigTest.php +++ b/packages/database/tests/Config/DatabaseConfigTest.php @@ -5,6 +5,8 @@ namespace Tempest\Database\Tests\Config; use Generator; +use PDO; +use Pdo\Mysql; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; @@ -62,4 +64,58 @@ public static function provide_database_drivers(): Generator 'secret', ]; } + + #[DataProvider('provide_database_drivers_with_options')] + #[Test] + public function driver_supports_pdo_options(DatabaseConfig $driver, array $expectedOptions): void + { + $this->assertSame($expectedOptions, $driver->options); + } + + public static function provide_database_drivers_with_options(): Generator + { + yield 'mysql with SSL' => [ + new MysqlConfig( + certificateAuthority: '/etc/ssl/certs/ca-certificates.crt', + persistent: true, + ), + [ + PDO::ATTR_PERSISTENT => true, + Mysql::ATTR_SSL_CA => '/etc/ssl/certs/ca-certificates.crt', + ], + ]; + + yield 'mysql with all SSL options' => [ + new MysqlConfig( + certificateAuthority: '/etc/ssl/certs/ca-certificates.crt', + verifyServerCertificate: false, + clientCertificate: '/path/to/cert.pem', + clientKey: '/path/to/key.pem', + ), + [ + Mysql::ATTR_SSL_CA => '/etc/ssl/certs/ca-certificates.crt', + Mysql::ATTR_SSL_VERIFY_SERVER_CERT => false, + Mysql::ATTR_SSL_CERT => '/path/to/cert.pem', + Mysql::ATTR_SSL_KEY => '/path/to/key.pem', + ], + ]; + + yield 'postgresql with persistent' => [ + new PostgresConfig( + persistent: true, + ), + [ + PDO::ATTR_PERSISTENT => true, + ], + ]; + + yield 'sqlite with persistent' => [ + new SQLiteConfig( + persistent: true, + ), + [ + PDO::ATTR_PERSISTENT => true, + ], + ]; + } }