diff --git a/CHANGELOG-v3.4.md b/CHANGELOG-v3.4.md index 9f4dfba15f2..35a16515454 100644 --- a/CHANGELOG-v3.4.md +++ b/CHANGELOG-v3.4.md @@ -7,6 +7,8 @@ - Added `craft\events\DefineGqlValidationRulesEvent`. - Added `craft\events\RegisterGqlPermissionsEvent`. - Added `craft\gql\TypeManager`. +- Added `craft\helpers\Db::parseDsn()`. +- Added `craft\helpers\Db::url2config()`. - Added `craft\services\Gql::getValidationRules()`. - Added `craft\web\Controller::requireGuest()`. - Added `craft\web\User::guestRequired()`. @@ -25,5 +27,10 @@ - Plugins can now modify the GraphQL schema by listening for the `defineGqlTypeFields` event. - Plugins can now modify the GraphQL permissions by listening for the `registerGqlPermissions` event. - Full GraphQL schema is now always generated when `devMode` is set to `true`. +- The installer now requires `config/db.php` to be setting the `dsn` database config setting with a `DB_DSN` environment variable, if a connection can’t already be established. - `craft\services\Elements::saveElement()` now has an `$updateSearchIndex` argument (defaults to `true`). ([#4840](https://github.com/craftcms/cms/issues/4840)) - `craft\services\Elements::resaveElements()` now has an `$updateSearchIndex` argument (defaults to `false`). ([#4840](https://github.com/craftcms/cms/issues/4840)) + +### Deprecated +- Deprecated the `url`, `driver`, `database`, `server`, `port`, and `unixSocket` database config settings. `dsn` should be used instead. +- Deprecated `craft\config\DbConfig::updateDsn()`. diff --git a/src/config/DbConfig.php b/src/config/DbConfig.php index 40304223341..d12cf6a728c 100644 --- a/src/config/DbConfig.php +++ b/src/config/DbConfig.php @@ -7,6 +7,8 @@ namespace craft\config; +use Craft; +use craft\helpers\Db; use craft\helpers\StringHelper; use yii\base\BaseObject; use yii\base\InvalidConfigException; @@ -50,39 +52,25 @@ class DbConfig extends BaseObject */ public $charset = 'utf8'; /** - * @var string The name of the database to select. - */ - public $database = ''; - /** - * @var string The database driver to use. Either 'mysql' for MySQL or 'pgsql' for PostgreSQL. - */ - public $driver = self::DRIVER_MYSQL; - /** - * @var string If you want to manually specify your PDO DSN connection string you can do so here. + * @var string The Data Source Name (“DSN”) that tells Craft how to connect to the database. + * + * DSNs should begin with a driver prefix (`mysql:` or `pgsql:`), followed by driver-specific parameters. + * For example, `mysql:host=127.0.0.1;port=3306;dbname=acme_corp`. * - * - MySQL: http://php.net/manual/en/ref.pdo-mysql.connection.php - * - PostgreSQL: http://php.net/manual/en/ref.pdo-pgsql.connection.php - * If you set this, then the [[server]], [[port]], [[user]], [[password]], [[database]], - * [[driver]] and [[unixSocket]] config settings will be ignored. + * - MySQL parameters: http://php.net/manual/en/ref.pdo-mysql.connection.php + * - PostgreSQL parameters: http://php.net/manual/en/ref.pdo-pgsql.connection.php */ public $dsn; /** * @var string The database password to connect with. */ public $password = ''; - /** - * @var int The database server port. Defaults to 3306 for MySQL and 5432 for PostgreSQL. - */ - public $port; /** * @var string The schema that Postgres is configured to use by default (PostgreSQL only). * @see https://www.postgresql.org/docs/8.2/static/ddl-schemas.html */ public $schema = 'public'; /** - * @var string The database server name or IP address. Usually 'localhost' or '127.0.0.1'. - */ - public $server = 'localhost'; /** * @var string If you're sharing Craft installs in a single database (MySQL) or a single * database and using a shared schema (PostgreSQL), then you can set a table @@ -91,22 +79,48 @@ class DbConfig extends BaseObject */ public $tablePrefix = ''; /** - * @var string|null MySQL only. If this is set, then the CLI connection string (used for yiic) will - * connect to the Unix socket, instead of the server and port. If this is - * specified, then 'server' and 'port' settings are ignored. + * @var string The database username to connect with. */ - public $unixSocket; + public $user = 'root'; + + // Deprecated Properties + // ------------------------------------------------------------------------- + /** * @var string|null The database connection URL, if one was provided by your hosting environment. * * If this is set, the values for [[driver]], [[user]], [[database]], [[server]], [[port]], and [[database]] * will be extracted from it. + * @deprecated in 3.4.0. Use [[Db::url2config()]] instead. */ public $url; /** - * @var string The database username to connect with. + * @var string The database driver to use. Either 'mysql' for MySQL or 'pgsql' for PostgreSQL. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead. */ - public $user = 'root'; + public $driver; + /** + * @var string The database server name or IP address. Usually 'localhost' or '127.0.0.1'. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead. + */ + public $server; + /** + * @var int The database server port. Defaults to 3306 for MySQL and 5432 for PostgreSQL. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead. + */ + public $port; + /** + * @var string|null MySQL only. If this is set, then the CLI connection string (used for yiic) will + * connect to the Unix socket, instead of the server and port. If this is + * specified, then 'server' and 'port' settings are ignored. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead, which can have a `unix_socket` param. + */ + public $unixSocket; + /** + * @var string The name of the database to select. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead. + */ + public $database; // Public Methods // ========================================================================= @@ -116,76 +130,9 @@ class DbConfig extends BaseObject */ public function init() { - // If the DSN is already set, parse it - if ($this->dsn) { - if (($pos = strpos($this->dsn, ':')) === false) { - throw new InvalidConfigException('Invalid DSN: ' . $this->dsn); - } - $this->driver = substr($this->dsn, 0, $pos); - $params = substr($this->dsn, $pos + 1); - foreach (explode(';', $params) as $param) { - if (($pos = strpos($param, '=')) === false) { - throw new InvalidConfigException('Invalid DSN param: ' . $param); - } - $paramName = substr($param, 0, $pos); - $paramValue = substr($param, $pos + 1); - switch ($paramName) { - case 'host': - $this->server = $paramValue; - break; - case 'port': - $this->port = $paramValue; - break; - case 'dbname': - $this->database = $paramValue; - break; - case 'unix_socket': - $this->unixSocket = $paramValue; - break; - case 'charset': - $this->charset = $paramValue; - break; - case 'user': // PG only - $this->user = $paramValue; - break; - case 'password': // PG only - $this->password = $paramValue; - break; - } - } - } - // If $url was set, parse it to set other properties if ($this->url) { - $url = parse_url($this->url); - if (isset($url['scheme'])) { - $scheme = strtolower($url['scheme']); - if (in_array($scheme, [self::DRIVER_PGSQL, 'postgres', 'postgresql'], true)) { - $this->driver = self::DRIVER_PGSQL; - } else { - $this->driver = self::DRIVER_MYSQL; - } - } - if (isset($url['user'])) { - $this->user = $url['user']; - } - if (isset($url['pass'])) { - $this->password = $url['pass']; - } - if (isset($url['host'])) { - $this->server = $url['host']; - } - if (isset($url['port'])) { - $this->port = $url['port']; - } - if (isset($url['path'])) { - $this->database = trim($url['path'], '/'); - } - } - - // Validate driver - if (!in_array($this->driver, [self::DRIVER_MYSQL, self::DRIVER_PGSQL], true)) { - throw new InvalidConfigException('Unsupported DB driver value: ' . $this->driver); + Craft::configure($this, Db::url2config($this->url)); } // Validate tablePrefix @@ -196,13 +143,34 @@ public function init() } } - // Lowercase server & unixSocket - $this->server = strtolower($this->server); - if ($this->unixSocket !== null) { + // If we don't have a DSN yet, create one from the deprecated settings + if ($this->dsn === null) { + $this->updateDsn(); + } + } + + /** + * Updates the DSN string based on the config setting values. + * @throws InvalidConfigException if [[driver]] isn’t set to `mysql` or `pgsql`. + * @deprecated in 3.4.0. [[dsn]] should be set directly instead. + */ + public function updateDsn() + { + if (!$this->driver) { + $this->driver = self::DRIVER_MYSQL; + } + + if (!in_array($this->driver, [self::DRIVER_MYSQL, self::DRIVER_PGSQL], true)) { + throw new InvalidConfigException('Unsupported DB driver value: ' . $this->driver); + } + + if ($this->driver === self::DRIVER_MYSQL && $this->unixSocket) { $this->unixSocket = strtolower($this->unixSocket); + $this->dsn = "{$this->driver}:unix_socket={$this->unixSocket};dbname={$this->database}"; + return; } - // Set the port + $this->server = strtolower($this->server ?? ''); if ($this->port === null || $this->port === '') { switch ($this->driver) { case self::DRIVER_MYSQL: @@ -212,27 +180,7 @@ public function init() $this->port = 5432; break; } - } else { - $this->port = (int)$this->port; - } - - // Set the DSN - if (!$this->dsn) { - $this->updateDsn(); - } - } - - /** - * Updates the DSN string based on the config setting values. - */ - public function updateDsn() - { - if (!$this->database) { - $this->dsn = null; - } else if ($this->driver === self::DRIVER_MYSQL && $this->unixSocket) { - $this->dsn = "{$this->driver}:unix_socket={$this->unixSocket};dbname={$this->database};"; - } else { - $this->dsn = "{$this->driver}:host={$this->server};dbname={$this->database};port={$this->port};"; } + $this->dsn = "{$this->driver}:host={$this->server};dbname={$this->database};port={$this->port}"; } } diff --git a/src/console/controllers/SetupController.php b/src/console/controllers/SetupController.php index a81a5483cf9..51e2e65c75d 100644 --- a/src/console/controllers/SetupController.php +++ b/src/console/controllers/SetupController.php @@ -47,7 +47,7 @@ class SetupController extends Controller /** * @var string|null The database username to connect with. */ - public $user; + public $user = 'root'; /** * @var string|null The database password to connect with. */ @@ -185,12 +185,6 @@ public function actionSecurityKey(): int */ public function actionDbCreds(): int { - try { - $dbConfig = Craft::$app->getConfig()->getDb(); - } catch (InvalidConfigException $e) { - $dbConfig = new DbConfig(); - } - $firstTime = true; $badUserCredentials = false; @@ -202,60 +196,46 @@ public function actionDbCreds(): int $this->stderr('--driver must be either "' . DbConfig::DRIVER_MYSQL . '" or "' . DbConfig::DRIVER_PGSQL . '".' . PHP_EOL, Console::FG_RED); return ExitCode::USAGE; } - $dbConfig->driver = $this->driver; } else if ($this->interactive) { - $dbConfig->driver = $this->select('Which database driver are you using?', [ + $this->driver = $this->select('Which database driver are you using?', [ DbConfig::DRIVER_MYSQL => 'MySQL', DbConfig::DRIVER_PGSQL => 'PostgreSQL', ]); } // server - if ($this->server) { - $server = $this->server; - } else { - $server = $this->prompt('Database server name or IP address:', [ - 'required' => true, - 'default' => $dbConfig->server ?: '127.0.0.1', - ]); - } - $dbConfig->server = strtolower($server); + $this->server = $this->prompt('Database server name or IP address:', [ + 'required' => true, + 'default' => $this->server ?: '127.0.0.1', + ]); + $this->server = strtolower($this->server); // port - if ($this->port) { - $dbConfig->port = (int)$this->port; + if ($firstTime) { + $defaultPort = $this->driver === DbConfig::DRIVER_MYSQL ? 3306 : 5432; } else { - if ($firstTime) { - $defaultPort = $dbConfig->driver === DbConfig::DRIVER_MYSQL ? 3306 : 5432; - } else { - $defaultPort = $dbConfig->port; - } - $dbConfig->port = (int)$this->prompt('Database port:', [ - 'required' => true, - 'default' => $defaultPort, - 'validator' => function(string $input): bool { - return is_numeric($input); - } - ]); + $defaultPort = $this->port; } + $this->port = $this->prompt('Database port:', [ + 'required' => true, + 'default' => $defaultPort, + 'validator' => function(string $input): bool { + return is_numeric($input); + } + ]); + $this->port = (int)$this->port; userCredentials: // user - if ($this->user) { - $dbConfig->user = $this->user; - } else { - $dbConfig->user = $this->prompt('Database username:', [ - 'default' => $dbConfig->user ?: null, - ]); - } + $this->user = $this->prompt('Database username:', [ + 'default' => $this->user ?: null, + ]); // password - if ($this->password) { - $dbConfig->password = $this->password; - } else if ($this->interactive) { + if ($this->interactive) { $this->stdout('Database password: '); - $dbConfig->password = CliPrompt::hiddenPrompt(true); + $this->password = CliPrompt::hiddenPrompt(true); } if ($badUserCredentials) { @@ -264,60 +244,57 @@ public function actionDbCreds(): int } // database - if ($this->database) { - $dbConfig->database = $this->database; - } else if ($this->interactive || $dbConfig->database) { - $dbConfig->database = $this->prompt('Database name:', [ - 'required' => true, - 'default' => $dbConfig->database ?: null, - ]); - } else { + if (!$this->interactive && !$this->database) { $this->stderr('The --database option must be set.' . PHP_EOL, Console::FG_RED); return ExitCode::USAGE; } + $this->database = $this->prompt('Database name:', [ + 'required' => true, + 'default' => $this->database ?: null, + ]); // schema - if ($dbConfig->driver === DbConfig::DRIVER_PGSQL) { - if ($this->schema) { - $dbConfig->schema = $this->schema; - } else { - $dbConfig->schema = $this->prompt('Database schema:', [ - 'required' => true, - 'default' => $dbConfig->schema ?: 'public', - ]); - } + if ($this->driver === DbConfig::DRIVER_PGSQL) { + $this->schema = $this->prompt('Database schema:', [ + 'required' => true, + 'default' => $this->schema ?: 'public', + ]); } // tablePrefix - if ($this->tablePrefix) { - $tablePrefix = $this->tablePrefix; - } else { - $tablePrefix = $this->prompt('Database table prefix' . ($dbConfig->tablePrefix ? ' (type "none" for none)' : '') . ':', [ - 'default' => $dbConfig->tablePrefix ?: null, - 'validator' => function(string $input): bool { - if (strlen(StringHelper::ensureRight($input, '_')) > 6) { - Console::stderr($this->ansiFormat('The table prefix must be 5 or less characters long.' . PHP_EOL, Console::FG_RED)); - return false; - } - return true; + $this->tablePrefix = $this->prompt('Database table prefix' . ($this->tablePrefix ? ' (type "none" for none)' : '') . ':', [ + 'default' => $this->tablePrefix ?: null, + 'validator' => function(string $input): bool { + if (strlen(StringHelper::ensureRight($input, '_')) > 6) { + Console::stderr($this->ansiFormat('The table prefix must be 5 or less characters long.' . PHP_EOL, Console::FG_RED)); + return false; } - ]); - } - if ($tablePrefix && $tablePrefix !== 'none') { - $dbConfig->tablePrefix = StringHelper::ensureRight($tablePrefix, '_'); + return true; + } + ]); + if ($this->tablePrefix && $this->tablePrefix !== 'none') { + $this->tablePrefix = StringHelper::ensureRight($this->tablePrefix, '_'); } else { - $tablePrefix = $dbConfig->tablePrefix = ''; + $this->tablePrefix = ''; } // Test the DB connection $this->stdout('Testing database credentials ... ', Console::FG_YELLOW); - $originalServer = $dbConfig->server; - $originalPort = $dbConfig->port; + try { + $dbConfig = Craft::$app->getConfig()->getDb(); + } catch (InvalidConfigException $e) { + $dbConfig = new DbConfig(); + } test: - $dbConfig->updateDsn(); + $dbConfig->dsn = "{$this->driver}:host={$this->server};port={$this->port};dbname={$this->database};"; + $dbConfig->user = $this->user; + $dbConfig->password = $this->password; + $dbConfig->schema = $this->schema; + $dbConfig->tablePrefix = $this->tablePrefix; + /** @var Connection $db */ $db = Craft::createObject(App::dbConfig($dbConfig)); @@ -339,18 +316,18 @@ public function actionDbCreds(): int // Test some common issues $message = $pdoException->getMessage(); - if ($dbConfig->server === 'localhost' && $message === 'SQLSTATE[HY000] [2002] No such file or directory') { + if ($this->server === 'localhost' && $message === 'SQLSTATE[HY000] [2002] No such file or directory') { // means the Unix socket doesn't exist - https://stackoverflow.com/a/22927341/1688568 // try 127.0.0.1 instead... $this->stdout('Trying with 127.0.0.1 instead of localhost ... ', Console::FG_YELLOW); - $dbConfig->server = '127.0.0.1'; + $this->server = '127.0.0.1'; goto test; } - if ($dbConfig->port === 3306 && $message === 'SQLSTATE[HY000] [2002] Connection refused') { + if ($this->port === 3306 && $message === 'SQLSTATE[HY000] [2002] Connection refused') { // try 8889 instead (default MAMP port)... $this->stdout('Trying with port 8889 instead of 3306 ... ', Console::FG_YELLOW); - $dbConfig->port = 8889; + $this->port = 8889; goto test; } @@ -368,10 +345,6 @@ public function actionDbCreds(): int return ExitCode::UNSPECIFIED_ERROR; } - // Restore the original server/port values - $dbConfig->server = $originalServer; - $dbConfig->port = $originalPort; - $firstTime = false; goto top; } @@ -383,14 +356,14 @@ public function actionDbCreds(): int $this->stdout('Saving database credentials to your .env file ... ', Console::FG_YELLOW); if ( - !$this->_setEnvVar('DB_DRIVER', $dbConfig->driver) || - !$this->_setEnvVar('DB_SERVER', $dbConfig->server) || - !$this->_setEnvVar('DB_PORT', $dbConfig->port) || - !$this->_setEnvVar('DB_USER', $dbConfig->user) || - !$this->_setEnvVar('DB_PASSWORD', $dbConfig->password) || - !$this->_setEnvVar('DB_DATABASE', $dbConfig->database) || - !$this->_setEnvVar('DB_SCHEMA', $dbConfig->schema) || - !$this->_setEnvVar('DB_TABLE_PREFIX', $tablePrefix) + !$this->_setEnvVar('DB_DRIVER', $this->driver) || + !$this->_setEnvVar('DB_SERVER', $this->server) || + !$this->_setEnvVar('DB_PORT', $this->port) || + !$this->_setEnvVar('DB_USER', $this->user) || + !$this->_setEnvVar('DB_PASSWORD', $this->password) || + !$this->_setEnvVar('DB_DATABASE', $this->database) || + !$this->_setEnvVar('DB_SCHEMA', $this->schema) || + !$this->_setEnvVar('DB_TABLE_PREFIX', $this->tablePrefix) ) { return ExitCode::UNSPECIFIED_ERROR; } diff --git a/src/controllers/InstallController.php b/src/controllers/InstallController.php index 9a30d2f5b70..27e945c6ba6 100644 --- a/src/controllers/InstallController.php +++ b/src/controllers/InstallController.php @@ -14,6 +14,7 @@ use craft\errors\DbConnectException; use craft\helpers\App; use craft\helpers\ArrayHelper; +use craft\helpers\Db; use craft\helpers\Install as InstallHelper; use craft\helpers\StringHelper; use craft\migrations\Install; @@ -127,17 +128,17 @@ public function actionValidateDb() $dbConfig = new DbConfig(); $this->_populateDbConfig($dbConfig); - + $parsed = Db::parseDsn($dbConfig->dsn); $errors = []; // Catch any low hanging fruit first - if (!$dbConfig->port) { + if (empty($parsed['port'])) { // Only possible if it was not numeric $errors['port'][] = Craft::t('yii', '{attribute} must be an integer.', [ 'attribute' => Craft::t('app', 'Port') ]); } - if (!$dbConfig->database) { + if (empty($parsed['dbname'])) { $errors['database'][] = Craft::t('yii', '{attribute} cannot be blank.', [ 'attribute' => Craft::t('app', 'Database Name') ]); @@ -148,7 +149,6 @@ public function actionValidateDb() if (empty($errors)) { // Test the connection - $dbConfig->updateDsn(); /** @var Connection $db */ $db = Craft::createObject(App::dbConfig($dbConfig)); @@ -246,14 +246,11 @@ public function actionInstall(): Response $dbConfig = Craft::$app->getConfig()->getDb(); $this->_populateDbConfig($dbConfig, 'db-'); - $configService->setDotEnvVar('DB_DRIVER', $dbConfig->driver); - $configService->setDotEnvVar('DB_SERVER', $dbConfig->server); + $configService->setDotEnvVar('DB_DSN', $dbConfig->dsn); $configService->setDotEnvVar('DB_USER', $dbConfig->user); $configService->setDotEnvVar('DB_PASSWORD', $dbConfig->password); - $configService->setDotEnvVar('DB_DATABASE', $dbConfig->database); $configService->setDotEnvVar('DB_SCHEMA', $dbConfig->schema); $configService->setDotEnvVar('DB_TABLE_PREFIX', $dbConfig->tablePrefix); - $configService->setDotEnvVar('DB_PORT', $dbConfig->port); // Update the db component based on new values /** @var Connection $db */ @@ -330,14 +327,9 @@ private function _canControlDbConfig(): bool // Map the DB settings we definitely care about to their environment variable names $vars = [ - 'driver' => 'DB_DRIVER', - 'server' => 'DB_SERVER', + 'dsn' => 'DB_DSN', 'user' => 'DB_USER', 'password' => 'DB_PASSWORD', - 'database' => 'DB_DATABASE', - //'DB_SCHEMA', - //'DB_TABLE_PREFIX', - //'DB_PORT', ]; // Save the current environment variable values, and set temporary ones @@ -382,17 +374,18 @@ private function _populateDbConfig(DbConfig $dbConfig, string $prefix = '') { $request = Craft::$app->getRequest(); - $dbConfig->dsn = null; - $dbConfig->url = null; - $dbConfig->driver = $request->getRequiredBodyParam($prefix . 'driver'); - $dbConfig->server = $request->getBodyParam($prefix . 'server') ?: 'localhost'; - $dbConfig->port = $request->getBodyParam($prefix . 'port'); - $dbConfig->user = $request->getBodyParam($prefix . 'user') ?: 'root'; - $dbConfig->password = $request->getBodyParam($prefix . 'password'); - $dbConfig->database = $request->getBodyParam($prefix . 'database'); - $dbConfig->schema = $request->getBodyParam($prefix . 'schema') ?: 'public'; - $dbConfig->tablePrefix = $request->getBodyParam($prefix . 'tablePrefix'); - - $dbConfig->init(); + $driver = $request->getRequiredBodyParam("{$prefix}driver"); + $server = $request->getBodyParam("{$prefix}server") ?: 'localhost'; + $database = $request->getBodyParam("{$prefix}database"); + $port = $request->getBodyParam("{$prefix}port"); + if ($port === null || $port === '') { + $port = $driver === DbConfig::DRIVER_MYSQL ? 3306 : 5432; + } + + $dbConfig->dsn = "{$driver}:host={$server};port={$port};dbname={$database}"; + $dbConfig->user = $request->getBodyParam("{$prefix}user") ?: 'root'; + $dbConfig->password = $request->getBodyParam("{$prefix}password"); + $dbConfig->schema = $request->getBodyParam("{$prefix}schema") ?: 'public'; + $dbConfig->tablePrefix = $request->getBodyParam("{$prefix}tablePrefix"); } } diff --git a/src/controllers/TemplatesController.php b/src/controllers/TemplatesController.php index 76e8d4736fe..7199dbb614f 100644 --- a/src/controllers/TemplatesController.php +++ b/src/controllers/TemplatesController.php @@ -8,7 +8,9 @@ namespace craft\controllers; use Craft; +use craft\config\DbConfig; use craft\helpers\App; +use craft\helpers\Db; use craft\helpers\Template; use craft\web\Controller; use ErrorException; @@ -158,7 +160,7 @@ public function actionRequirementsCheck() $reqCheck = new \RequirementsChecker(); $dbConfig = Craft::$app->getConfig()->getDb(); $reqCheck->dsn = $dbConfig->dsn; - $reqCheck->dbDriver = $dbConfig->driver; + $reqCheck->dbDriver = $dbConfig->dsn ? Db::parseDsn($dbConfig->dsn, 'driver') : DbConfig::DRIVER_MYSQL; $reqCheck->dbUser = $dbConfig->user; $reqCheck->dbPassword = $dbConfig->password; diff --git a/src/db/Connection.php b/src/db/Connection.php index 6b784f86ed7..cec09c2f438 100644 --- a/src/db/Connection.php +++ b/src/db/Connection.php @@ -19,6 +19,7 @@ use craft\events\BackupEvent; use craft\events\RestoreEvent; use craft\helpers\App; +use craft\helpers\Db; use craft\helpers\FileHelper; use craft\helpers\StringHelper; use mikehaertl\shellcommand\Command as ShellCommand; @@ -553,15 +554,17 @@ private function _createShellCommand(string $command): ShellCommand */ private function _parseCommandTokens(string $command, $file): string { - $dbConfig = Craft::$app->getConfig()->getDb(); + $parsed = Db::parseDsn($this->dsn); + $username = $this->getIsPgsql() && !empty($parsed['user']) ? $parsed['user'] : $this->username; + $password = $this->getIsPgsql() && !empty($parsed['password']) ? $parsed['password'] : $this->password; $tokens = [ '{file}' => $file, - '{port}' => $dbConfig->port, - '{server}' => $dbConfig->server, - '{user}' => $dbConfig->user, - '{password}' => addslashes(str_replace('$', '\\$', $dbConfig->password)), - '{database}' => $dbConfig->database, - '{schema}' => $dbConfig->schema, + '{port}' => $parsed['port'] ?? '', + '{server}' => $parsed['host'] ?? '', + '{user}' => $username, + '{password}' => addslashes(str_replace('$', '\\$', $password)), + '{database}' => $parsed['dbname'] ?? '', + '{schema}' => $this->getSchema()->defaultSchema ?? '', ]; return str_replace(array_keys($tokens), $tokens, $command); diff --git a/src/db/mysql/Schema.php b/src/db/mysql/Schema.php index fc24017fa1f..a7cbedd74f8 100644 --- a/src/db/mysql/Schema.php +++ b/src/db/mysql/Schema.php @@ -9,6 +9,7 @@ use Craft; use craft\db\TableSchema; +use craft\helpers\Db; use craft\helpers\FileHelper; use yii\db\Exception; @@ -310,15 +311,17 @@ private function _createDumpConfigFile(): string { $filePath = Craft::$app->getPath()->getTempPath() . DIRECTORY_SEPARATOR . 'my.cnf'; - $dbConfig = Craft::$app->getConfig()->getDb(); + $parsed = Db::parseDsn($this->db->dsn); + $username = $this->db->getIsPgsql() && !empty($parsed['user']) ? $parsed['user'] : $this->db->username; + $password = $this->db->getIsPgsql() && !empty($parsed['password']) ? $parsed['password'] : $this->db->password; $contents = '[client]' . PHP_EOL . - 'user=' . $dbConfig->user . PHP_EOL . - 'password="' . addslashes($dbConfig->password) . '"' . PHP_EOL . - 'host=' . $dbConfig->server . PHP_EOL . - 'port=' . $dbConfig->port; + 'user=' . $username . PHP_EOL . + 'password="' . addslashes($password) . '"' . PHP_EOL . + 'host=' . ($parsed['host'] ?? '') . PHP_EOL . + 'port=' . ($parsed['port'] ?? ''); - if ($dbConfig->unixSocket) { - $contents .= PHP_EOL . 'socket=' . $dbConfig->unixSocket; + if (isset($parsed['unix_socket'])) { + $contents .= PHP_EOL . 'socket=' . $parsed['unix_socket']; } FileHelper::writeToFile($filePath, $contents); diff --git a/src/helpers/App.php b/src/helpers/App.php index c673e9d6182..581117c0a36 100644 --- a/src/helpers/App.php +++ b/src/helpers/App.php @@ -394,7 +394,9 @@ public static function dbConfig(DbConfig $dbConfig = null): array $dbConfig = Craft::$app->getConfig()->getDb(); } - if ($dbConfig->driver === DbConfig::DRIVER_MYSQL) { + $driver = $dbConfig->dsn ? Db::parseDsn($dbConfig->dsn, 'driver') : DbConfig::DRIVER_MYSQL; + + if ($driver === DbConfig::DRIVER_MYSQL) { $schemaConfig = [ 'class' => MysqlSchema::class, ]; @@ -407,17 +409,17 @@ public static function dbConfig(DbConfig $dbConfig = null): array return [ 'class' => Connection::class, - 'driverName' => $dbConfig->driver, + 'driverName' => $driver, 'dsn' => $dbConfig->dsn, 'username' => $dbConfig->user, 'password' => $dbConfig->password, 'charset' => $dbConfig->charset, 'tablePrefix' => $dbConfig->tablePrefix, 'schemaMap' => [ - $dbConfig->driver => $schemaConfig, + $driver => $schemaConfig, ], 'commandMap' => [ - $dbConfig->driver => Command::class, + $driver => Command::class, ], 'attributes' => $dbConfig->attributes, 'enableSchemaCache' => !YII_DEBUG, diff --git a/src/helpers/Db.php b/src/helpers/Db.php index 46afab3c13d..3ef8d16cd66 100644 --- a/src/helpers/Db.php +++ b/src/helpers/Db.php @@ -9,10 +9,12 @@ use Craft; use craft\base\Serializable; +use craft\config\DbConfig; use craft\db\Connection; use craft\db\mysql\Schema as MysqlSchema; use craft\db\Query; use yii\base\Exception; +use yii\base\InvalidArgumentException; use yii\base\NotSupportedException; use yii\db\Schema; @@ -741,6 +743,105 @@ public static function uidsByIds(string $table, array $ids): array ->pairs(); } + /** + * Parses a DSN string and returns an array with the `driver` and any driver params, or just a single key. + * + * @param string $dsn + * @param string|null $key The key that is needed from the DSN. If this is + * @return array|string|false The full array, or the specific key value, or `false` if `$key` is a param that + * doesn’t exist in the DSN string. + * @throws InvalidArgumentException if $dsn is invalid + * @since 3.4.0 + */ + public static function parseDsn(string $dsn, string $key = null) + { + if (($pos = strpos($dsn, ':')) === false) { + throw new InvalidArgumentException('Invalid DSN: ' . $dsn); + } + + $driver = strtolower(substr($dsn, 0, $pos)); + if ($key === 'driver') { + return $driver; + } + if ($key === null) { + $parsed = [ + 'driver' => $driver, + ]; + } + + $params = substr($dsn, $pos + 1); + foreach (ArrayHelper::filterEmptyStringsFromArray(explode(';', $params)) as $param) { + list($n, $v) = array_pad(explode('=', $param, 2), 2, ''); + if ($key === $n) { + return $v; + } + if ($key === null) { + $parsed[$n] = $v; + } + } + if ($key === null) { + return $parsed; + } + return false; + } + + /** + * Generates a DB config from a database connection URL. + * + * This can be used from `config/db.php`: + * --- + * ```php + * $url = getenv('DB_URL'); + * return craft\helpers\Db::url2config($url); + * ``` + * + * @param string $url + * @return array + * @since 3.4.0 + */ + public static function url2config(string $url): array + { + $parsed = parse_url($url); + + if (!isset($parsed['scheme'])) { + throw new InvalidArgumentException('Invalid URL: ' . $url); + } + + $config = []; + + // user & password + if (isset($parsed['user'])) { + $config['user'] = $parsed['user']; + } + if (isset($parsed['pass'])) { + $config['password'] = $parsed['pass']; + } + + // URL scheme => driver + if (in_array(strtolower($parsed['scheme']), ['pgsql', 'postgres', 'postgresql'], true)) { + $driver = DbConfig::DRIVER_PGSQL; + } else { + $driver = DbConfig::DRIVER_MYSQL; + } + + // DSN params + $checkParams = [ + 'host' => 'host', + 'port' => 'port', + 'path' => 'dbname', + ]; + $dsnParams = []; + foreach ($checkParams as $urlParam => $dsnParam) { + if (isset($parsed[$urlParam])) { + $dsnParams[] = "{$dsnParam}={$parsed[$urlParam]}"; + } + } + + $config['dsn'] = "{$driver}:" . implode(';', $dsnParams); + + return $config; + } + // Private Methods // ========================================================================= diff --git a/src/test/Craft.php b/src/test/Craft.php index 412b446e46e..e93ec1264bf 100644 --- a/src/test/Craft.php +++ b/src/test/Craft.php @@ -336,14 +336,11 @@ public static function normalizePathSeparators($path) public static function createDbConfig(): DbConfig { return new DbConfig([ - 'password' => getenv('DB_PASSWORD'), + 'dsn' => getenv('DB_DSN'), 'user' => getenv('DB_USER'), - 'database' => getenv('DB_DATABASE'), + 'password' => getenv('DB_PASSWORD'), 'tablePrefix' => getenv('DB_TABLE_PREFIX'), - 'driver' => getenv('DB_DRIVER'), - 'port' => getenv('DB_PORT'), 'schema' => getenv('DB_SCHEMA'), - 'server' => getenv('DB_SERVER'), ]); } diff --git a/src/test/internal/example-test-suite/tests/.env.example.mysql b/src/test/internal/example-test-suite/tests/.env.example.mysql index bda549b78ac..37649e92eac 100644 --- a/src/test/internal/example-test-suite/tests/.env.example.mysql +++ b/src/test/internal/example-test-suite/tests/.env.example.mysql @@ -1,11 +1,8 @@ # Set in accordance to your environment -DB_DRIVER="mysql" -DB_SERVER="localhost" +DB_DSN="mysql:host=localhost;port=3306;dbname=craft-test" DB_USER="root" DB_PASSWORD="" -DB_DATABASE="craft-test" -DB_SCHEMA="" DB_TABLE_PREFIX="craft" -DB_PORT="3306" + SECURITY_KEY="abcde12345" diff --git a/src/test/internal/example-test-suite/tests/.env.example.pgsql b/src/test/internal/example-test-suite/tests/.env.example.pgsql index 6cb16cc1d1e..efb42177eb0 100644 --- a/src/test/internal/example-test-suite/tests/.env.example.pgsql +++ b/src/test/internal/example-test-suite/tests/.env.example.pgsql @@ -1,11 +1,9 @@ # Set in accordance to your environment -DB_DRIVER="pgsql" -DB_SERVER="localhost" +DB_DSN="pgsql:host=localhost;port=5432;dbname=craft-test" DB_USER="postgres" DB_PASSWORD="" -DB_DATABASE="craft-test" DB_SCHEMA="public" -DB_TABLE_PREFIX="" -DB_PORT="5432" +DB_TABLE_PREFIX="craft" + SECURITY_KEY="abcde12345" diff --git a/src/test/internal/example-test-suite/tests/_craft/config/db.php b/src/test/internal/example-test-suite/tests/_craft/config/db.php index 7357a6c750f..64c3ecfca83 100644 --- a/src/test/internal/example-test-suite/tests/_craft/config/db.php +++ b/src/test/internal/example-test-suite/tests/_craft/config/db.php @@ -1,12 +1,9 @@ getenv('DB_PASSWORD'), + 'dsn' => getenv('DB_DSN'), 'user' => getenv('DB_USER'), - 'database' => getenv('DB_DATABASE'), - 'tablePrefix' => getenv('DB_TABLE_PREFIX'), - 'driver' => getenv('DB_DRIVER'), - 'port' => getenv('DB_PORT'), + 'password' => getenv('DB_PASSWORD'), 'schema' => getenv('DB_SCHEMA'), - 'server' => getenv('DB_SERVER'), + 'tablePrefix' => getenv('DB_TABLE_PREFIX'), ];