diff --git a/wire/config.php b/wire/config.php index 3d59e3ff..a574d6af 100644 --- a/wire/config.php +++ b/wire/config.php @@ -820,6 +820,37 @@ $config->dbHost = ''; */ $config->dbPort = 3306; +/** + * Database init command (PDO::MYSQL_ATTR_INIT_COMMAND) + * + * Note: Placeholder "{charset}" gets automatically replaced with $config->dbCharset. + * + * @var string + * + */ +$config->dbInitCommand = "SET NAMES '{charset}'"; + +/** + * Set or adjust SQL mode per MySQL version + * + * Array indexes are minimum MySQL version mode applies to. Array values are + * the names of the mode(s) to apply. If value is preceded with "remove:" the mode will + * be removed, or if preceded with "add:" the mode will be added. If neither is present + * then the mode will be set exactly as given. To specify more than one SQL mode for the + * value, separate them by commas (CSV). To specify multiple statements for the same + * version, separate them with a slash "/". + * + * ~~~~~ + * array("5.7.0" => "remove:STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY/add:NO_ZERO_DATE") + * ~~~~~ + * + * @var array + * + */ +$config->dbSqlModes = array( + "5.7.0" => "remove:STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY" +); + /** * Optional DB socket config for sites that need it (for most you should exclude this) * diff --git a/wire/core/Config.php b/wire/core/Config.php index aea9cca9..b2394544 100644 --- a/wire/core/Config.php +++ b/wire/core/Config.php @@ -99,6 +99,8 @@ * @property string $dbEngine Database engine (MyISAM or InnoDB) #pw-group-database * @property string $dbPath MySQL database exec path (Path to mysqldump) #pw-group-database * @property int $dbQueryLogMax Maximum number of queries WireDatabasePDO will log in memory, when debug mode is enabled (default=1000). #pw-group-database + * @property string $dbInitCommand Database init command, for PDO::MYSQL_ATTR_INIT_COMMAND. Note placeholder {charset} gets replaced with $config->dbCharset. #pw-group-database + * $property array $dbSqlModes Set, add or remove SQL mode based on MySQL version. See default in /wire/config.php for details. #pw-group-database * * @property array $pageList Settings specific to Page lists. #pw-group-modules * @property array $pageEdit Settings specific to Page editors. #pw-group-modules diff --git a/wire/core/WireDatabasePDO.php b/wire/core/WireDatabasePDO.php index 34831ede..9c6f7916 100644 --- a/wire/core/WireDatabasePDO.php +++ b/wire/core/WireDatabasePDO.php @@ -55,6 +55,14 @@ class WireDatabasePDO extends Wire implements WireDatabase { */ protected $pdo = null; + /** + * Whether or not our _init() has been called for the current $pdo connection + * + * @var bool + * + */ + protected $init = false; + /** * PDO connection settings * @@ -99,6 +107,8 @@ class WireDatabasePDO extends Wire implements WireDatabase { $name = $config->dbName; $socket = $config->dbSocket; $charset = $config->dbCharset; + $initCommand = str_replace('{charset}', $charset, $config->dbInitCommand); + if($socket) { // if socket is provided ignore $host and $port and use $socket instead: $dsn = "mysql:unix_socket=$socket;dbname=$name;"; @@ -107,13 +117,18 @@ class WireDatabasePDO extends Wire implements WireDatabase { $port = $config->dbPort; if($port) $dsn .= ";port=$port"; } + $driver_options = array( - \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES '$charset'", \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION - ); + ); + + if($initCommand) $driver_options[\PDO::MYSQL_ATTR_INIT_COMMAND] = $initCommand; + $database = new WireDatabasePDO($dsn, $username, $password, $driver_options); $database->setDebugMode($config->debug); $config->wire($database); + $database->_init(); + return $database; } @@ -134,7 +149,37 @@ class WireDatabasePDO extends Wire implements WireDatabase { $this->pdoConfig['pass'] = $password; $this->pdoConfig['options'] = $driver_options; $this->pdo(); - $this->queryLogMax = (int) $this->wire('config')->dbQueryLogMax; + } + + /** + * Additional initialization after DB connection established and Wire instance populated + * + * #pw-internal + * + */ + public function _init() { + if($this->init || !$this->isWired()) return; + $this->init = true; + $config = $this->wire('config'); + $this->queryLogMax = (int) $config->dbQueryLogMax; + $sqlModes = $config->dbSqlModes; + if(is_array($sqlModes)) { + // ["5.7.0" => "remove:mode1,mode2/add:mode3"] + foreach($sqlModes as $minVersion => $commands) { + if(strpos($commands, '/') !== false) { + $commands = explode('/', $commands); + } else { + $commands = array($commands); + } + foreach($commands as $modes) { + $modes = trim($modes); + if(empty($modes)) continue; + $action = 'set'; + if(strpos($modes, ':')) list($action, $modes) = explode(':', $modes); + $this->sqlMode(trim($action), trim($modes), $minVersion); + } + } + } } /** @@ -148,12 +193,16 @@ class WireDatabasePDO extends Wire implements WireDatabase { * */ public function pdo() { - if(!$this->pdo) $this->pdo = new \PDO( - $this->pdoConfig['dsn'], - $this->pdoConfig['user'], - $this->pdoConfig['pass'], - $this->pdoConfig['options'] - ); + if(!$this->pdo) { + $this->init = false; + $this->pdo = new \PDO( + $this->pdoConfig['dsn'], + $this->pdoConfig['user'], + $this->pdoConfig['pass'], + $this->pdoConfig['options'] + ); + } + if(!$this->init) $this->_init(); return $this->pdo; } @@ -731,4 +780,73 @@ class WireDatabasePDO extends Wire implements WireDatabase { return $max; } + /** + * Get SQL mode, set SQL mode, add to existing SQL mode, or remove from existing SQL mode + * + * #pw-group-custom + * + * ~~~~~ + * // Get SQL mode + * $mode = $database->sqlMode(); + * + * // Add an SQL mode + * $database->sqlMode('add', 'STRICT_TRANS_TABLES'); + * + * // Remove SQL mode if version at least 5.7.0 + * $database->sqlMode('remove', 'ONLY_FULL_GROUP_BY', '5.7.0'); + * ~~~~~ + * + * @param string $action Specify "get", "set", "add" or "remove". (default="get") + * @param string $mode Mode string or CSV string with SQL mode(s), i.e. "STRICT_TRANS_TABLES,ONLY_FULL_GROUP_BY". + * This argument should be omitted when using the "get" action. + * @param string $minVersion Make the given action only apply if MySQL version is at least $minVersion, i.e. "5.7.0". + * @return string|bool Returns string in "get" action, boolean false if required version not present, or true otherwise. + * @throws WireException If given an invalid $action + * + */ + public function sqlMode($action = 'get', $mode = '', $minVersion = '') { + + $result = true; + $modes = array(); + + if(empty($action)) $action = 'get'; + + if($action !== 'get' && $minVersion) { + $serverVersion = $this->getAttribute(\PDO::ATTR_SERVER_VERSION); + if(version_compare($serverVersion, $minVersion, '<')) return false; + } + + if($mode) { + foreach(explode(',', $mode) as $m) { + $modes[] = $this->escapeStr(strtoupper($this->wire('sanitizer')->fieldName($m))); + } + } + + switch($action) { + case 'get': + $query = $this->pdo()->query("SELECT @@sql_mode"); + $result = $query->fetchColumn(); + $query->closeCursor(); + break; + case 'set': + $modes = implode(',', $modes); + $result = $modes; + $this->pdo()->exec("SET sql_mode='$modes'"); + break; + case 'add': + foreach($modes as $m) { + $this->pdo()->exec("SET sql_mode=(SELECT CONCAT(@@sql_mode,',$m'))"); + } + break; + case 'remove': + foreach($modes as $m) { + $this->pdo()->exec("SET sql_mode=(SELECT REPLACE(@@sql_mode,'$m',''))"); + } + break; + default: + throw new WireException("Unknown action '$action'"); + } + + return $result; + } }