3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2011-2023 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2011-2023 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2011-07-27
37 namespace Galette\Core
;
41 use Laminas\Db\Adapter\Adapter
;
42 use Laminas\Db\Adapter\Driver\DriverInterface
;
43 use Laminas\Db\Adapter\Driver\AbstractConnection
;
44 use Laminas\Db\Adapter\Platform\PlatformInterface
;
45 use Laminas\Db\Sql\Insert
;
46 use Laminas\Db\Sql\Update
;
47 use Laminas\Db\Sql\Select
;
48 use Laminas\Db\Sql\Delete
;
49 use Laminas\Db\ResultSet\ResultSet
;
50 use Laminas\Db\Sql\Sql
;
51 use Laminas\Db\Sql\SqlInterface
;
59 * @author Johan Cwiklinski <johan@x-tnd.be>
60 * @copyright 2011-2023 The Galette Team
61 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
62 * @link http://framework.zend.com/apidoc/2.2/namespaces/Zend.Db.html
63 * @since Available since 0.7dev - 2011-07-27
65 * @property Adapter $db
67 * @property DriverInterface $driver
68 * @property AbstractConnection $connection
69 * @property PlatformInterface $platform
70 * @property string $query_string
71 * @property string $type_db
86 public const MYSQL
= 'mysql';
87 public const PGSQL
= 'pgsql';
89 public const MYSQL_DEFAULT_PORT
= 3306;
90 public const PGSQL_DEFAULT_PORT
= 5432;
95 * @param array $dsn Connection information
96 * If not set, database constants will be used.
98 public function __construct($dsn = null)
102 if ($dsn !== null && is_array($dsn)) {
103 $_type_db = $dsn['TYPE_DB'];
104 $_host_db = $dsn['HOST_DB'];
105 $_port_db = $dsn['PORT_DB'];
106 $_user_db = $dsn['USER_DB'];
107 $_pwd_db = $dsn['PWD_DB'];
108 $_name_db = $dsn['NAME_DB'];
119 if ($_type_db === self
::MYSQL
) {
120 $_type = 'Pdo_Mysql';
121 } elseif ($_type_db === self
::PGSQL
) {
122 $_type = 'Pdo_Pgsql';
124 throw new \
Exception("Type $_type_db not known (dsn: $_user_db@$_host_db(:$_port_db)/$_name_db)");
127 $this->type_db
= $_type_db;
128 $this->options
= array(
130 'hostname' => $_host_db,
132 'username' => $_user_db,
133 'password' => $_pwd_db,
134 'database' => $_name_db
136 if ($_type_db === self
::MYSQL
&& !defined('NON_UTF_DBCONNECT')) {
137 $this->options
['charset'] = 'utf8';
140 $this->doConnection();
141 } catch (Throwable
$e) {
142 // perhaps factory() failed to load the specified Adapter class
144 '[Db] Error (' . $e->getCode() . '|' .
145 $e->getMessage() . ')',
153 * Do database connection
157 private function doConnection()
159 $this->db
= new Adapter($this->options
);
160 $this->db
->getDriver()->getConnection()->connect();
161 $this->sql
= new Sql($this->db
);
163 if (!$this->isPostgres()) {
164 $this->db
->query("SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));");
169 * To store Db in session
173 public function __sleep()
175 return ['type_db', 'options'];
179 * Connect again to the database on wakeup
183 public function __wakeup()
185 $this->doConnection();
189 * Retrieve current database version
191 * @param boolean $check_table Check if table exists, defaults to false
195 * @throw LogicException
197 public function getDbVersion($check_table = false)
200 if ($check_table === true) {
201 $exists = count($this->getTables(PREFIX_DB
. 'database')) === 1;
206 if ($exists === true) {
207 $select = $this->select('database');
212 $results = $this->execute($select);
213 $result = $results->current();
214 return number_format(
223 } catch (Throwable
$e) {
225 'Cannot check database version: ' . $e->getMessage(),
228 throw new \
LogicException('Cannot check database version');
233 * Check if database version suits our needs
237 public function checkDbVersion()
239 if (GALETTE_MODE
=== 'DEV') {
241 'Database version not checked in DEV mode.',
248 return $this->getDbVersion() === GALETTE_DB_VERSION
;
249 } catch (\LogicException
$e) {
255 * Peform a select query on the whole table
257 * @param string $table Table name
261 public function selectAll($table)
263 return $this->db
->query(
264 'SELECT * FROM ' . PREFIX_DB
. $table,
265 Adapter
::QUERY_MODE_EXECUTE
270 * Test if database can be contacted. Mostly used for installation
272 * @param string $type db type
273 * @param string $user database's user
274 * @param string $pass password for the user
275 * @param string $host which host we want to connect to
276 * @param string $port which tcp port we want to connect to
277 * @param string $db database name
281 * @throws \Exception|Throwable
283 public static function testConnectivity(
292 if ($type === self
::MYSQL
) {
293 $_type = 'Pdo_Mysql';
294 } elseif ($type === self
::PGSQL
) {
295 $_type = 'Pdo_Pgsql';
297 throw new \
Exception('Unknown database type');
309 $_db = new Adapter($_options);
310 $_db->getDriver()->getConnection()->connect();
313 } catch (Throwable
$e) {
314 // perhaps failed to load the specified Adapter class
316 '[' . __METHOD__
. '] Connection error (' . $e->getCode() . '|' .
317 $e->getMessage() . ')',
325 * Drop test table if it exists, so we can make all checks.
329 public function dropTestTable()
332 $this->db
->query('DROP TABLE IF EXISTS galette_test');
333 Analog
::log('Test table successfully dropped.', Analog
::DEBUG
);
334 } catch (Throwable
$e) {
336 'Cannot drop test table! ' . $e->getMessage(),
343 * Checks GRANT access for install time
345 * @param string $mode are we at install time (i) or update time (u) ?
347 * @return array containing each test. Each array entry could
348 * be either true or contains an exception of false if test did not
351 public function grantCheck($mode = 'i')
354 'Check for database rights (mode ' . $mode . ')',
367 $results['alter'] = false;
370 //can Galette CREATE tables?
372 $sql = 'CREATE TABLE galette_test (
373 test_id INTEGER NOT NULL,
374 test_text VARCHAR(20)
376 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
377 $results['create'] = true;
378 } catch (Throwable
$e) {
379 Analog
::log('Cannot CREATE TABLE', Analog
::WARNING
);
380 //if we cannot create tables, we cannot check other permissions
382 $results['create'] = $e;
385 //all those tests need the table to exists
388 //can Galette ALTER tables? (only for update mode)
390 $sql = 'ALTER TABLE galette_test ALTER test_text SET DEFAULT \'nothing\'';
391 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
392 $results['alter'] = true;
393 } catch (Throwable
$e) {
395 'Cannot ALTER TABLE | ' . $e->getMessage(),
398 $results['alter'] = $e;
402 //can Galette INSERT records ?
405 'test_text' => 'a simple text'
408 $insert = $this->sql
->insert('galette_test');
409 $insert->values($values);
411 $res = $this->execute($insert);
413 if ($res->count() === 1) {
414 $results['insert'] = true;
416 throw new \
Exception('No row inserted!');
418 } catch (Throwable
$e) {
420 'Cannot INSERT records | ' . $e->getMessage(),
423 //if we cannot insert records, some others tests cannot be done
425 $results['insert'] = $e;
428 //all those tests need that the first record exists
430 //can Galette UPDATE records ?
432 'test_text' => 'another simple text'
435 $update = $this->sql
->update('galette_test');
436 $update->set($values)->where(
437 array('test_id' => 1)
439 $res = $this->execute($update);
440 if ($res->count() === 1) {
441 $results['update'] = true;
443 throw new \
Exception('No row updated!');
445 } catch (Throwable
$e) {
447 'Cannot UPDATE records | ' . $e->getMessage(),
450 $results['update'] = $e;
453 //can Galette SELECT records ?
455 $select = $this->sql
->select('galette_test');
456 $select->where(['test_id' => 1]);
457 $res = $this->execute($select);
458 $pass = $res->count() === 1;
461 $results['select'] = true;
463 throw new \
Exception('Select is empty!');
465 } catch (Throwable
$e) {
467 'Cannot SELECT records | ' . $e->getMessage(),
470 $results['select'] = $e;
473 //can Galette DELETE records ?
475 $delete = $this->sql
->delete('galette_test');
476 $delete->where(array('test_id' => 1));
477 $this->execute($delete);
478 $results['delete'] = true;
479 } catch (Throwable
$e) {
481 'Cannot DELETE records | ' . $e->getMessage(),
484 $results['delete'] = $e;
488 //can Galette DROP tables ?
490 $sql = 'DROP TABLE galette_test';
491 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
492 $results['drop'] = true;
493 } catch (Throwable
$e) {
495 'Cannot DROP TABLE | ' . $e->getMessage(),
498 $results['drop'] = $e;
506 * Get a list of Galette's tables
508 * @param string $prefix Specified table prefix, PREFIX_DB if null
512 public function getTables($prefix = null)
514 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
515 $tmp_tables_list = $metadata->getTableNames();
517 if ($prefix === null) {
521 $tables_list = array();
522 //filter table_list: we only want PREFIX_DB tables
523 foreach ($tmp_tables_list as $t) {
524 if (preg_match('/^' . $prefix . '/', $t)) {
532 * Get columns for a specified table
534 * @param string $table Table name
538 public function getColumns($table)
540 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
541 $table = $metadata->getTable(PREFIX_DB
. $table);
542 return $table->getColumns();
546 * Converts recursively database to UTF-8
548 * @param string $prefix Specified table prefix
549 * @param boolean $content_only Proceed only content (no table conversion)
553 public function convertToUTF($prefix = null, $content_only = false)
555 if ($this->isPostgres()) {
557 'Cannot change encoding on PostgreSQL database',
562 if ($prefix === null) {
568 $tables = $this->getTables($prefix);
570 foreach ($tables as $table) {
571 if ($content_only === false) {
572 //Change whole table charset
573 //CONVERT TO instruction will take care of each fields,
574 //but converting data stay our problem.
575 $query = 'ALTER TABLE ' . $table .
576 ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci';
580 Adapter
::QUERY_MODE_EXECUTE
584 'Charset successfully changed for table `' . $table . '`',
590 if ($table != $prefix . 'pictures') {
591 $this->convertContentToUTF($prefix, $table);
594 } catch (Throwable
$e) {
596 'An error occurred while converting to utf table ' .
597 $table . ' (' . $e->getMessage() . ')',
605 * Converts dtabase content to UTF-8
607 * @param string $prefix Specified table prefix
608 * @param string $table the table we want to convert datas from
612 private function convertContentToUTF($prefix, $table)
616 $query = 'SET NAMES latin1';
619 Adapter
::QUERY_MODE_EXECUTE
621 } catch (Throwable
$e) {
623 'Cannot SET NAMES on table `' . $table . '`. ' .
630 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
631 $tbl = $metadata->getTable($table);
632 $constraints = $tbl->getConstraints();
635 foreach ($constraints as $constraint) {
636 if ($constraint->getType() === 'PRIMARY KEY') {
637 $pkeys = $constraint->getColumns();
641 if (count($pkeys) == 0) {
642 //no primary key! How to do an update without that?
643 //Prior to 0.7, l10n and dynamic_fields tables does not
644 //contains any primary key. Since encoding conversion is done
645 //_before_ the SQL upgrade, we'll have to manually
647 if (preg_match('/' . $prefix . 'dynamic_fields/', $table) !== 0) {
654 } elseif (preg_match('/' . $prefix . 'l10n/', $table) !== 0) {
660 //not a know case, we do not perform any update.
661 throw new \
Exception(
662 'Cannot define primary key for table `' . $table .
668 $select = $this->sql
->select($table);
669 $results = $this->execute($select);
671 foreach ($results as $row) {
676 foreach ($pkeys as $k) {
677 $where[$k] = $row->$k;
681 foreach ($row as $key => $value) {
682 $data[$key] = $value;
685 //finally, update data!
686 $update = $this->sql
->update($table);
687 $update->set($data)->where($where);
688 $this->execute($update);
690 } catch (Throwable
$e) {
692 'An error occurred while converting contents to UTF-8 for table ' .
693 $table . ' (' . $e->getMessage() . ')',
700 * Is current database using Postgresql?
704 public function isPostgres()
706 return $this->type_db
=== self
::PGSQL
;
710 * Instanciate a select query
712 * @param string $table Table name, without prefix
713 * @param string $alias Tables alias, optionnal
717 public function select($table, $alias = null)
719 if ($alias === null) {
720 return $this->sql
->select(
724 return $this->sql
->select(
726 $alias => PREFIX_DB
. $table
733 * Instanciate an insert query
735 * @param string $table Table name, without prefix
739 public function insert($table)
741 return $this->sql
->insert(
747 * Instanciate an update query
749 * @param string $table Table name, without prefix
753 public function update($table)
755 return $this->sql
->update(
761 * Instanciate a delete query
763 * @param string $table Table name, without prefix
767 public function delete($table)
769 return $this->sql
->delete(
775 * Execute query string
777 * @param SqlInterface $sql SQL object
781 public function execute($sql)
784 $query_string = $this->sql
->buildSqlString($sql);
785 $this->last_query
= $query_string;
786 $this->log($query_string);
787 return $this->db
->query(
789 Adapter
::QUERY_MODE_EXECUTE
791 } catch (Throwable
$e) {
792 $msg = 'Query error: ';
793 if (isset($query_string)) {
794 $msg .= $query_string;
797 $msg . ' ' . $e->__toString(),
800 if ($sql instanceof Insert
&& $this->isDuplicateException($e)) {
801 throw new \
OverflowException('Duplicate entry', 0, $e);
808 * Global getter method
810 * @param string $name name of the variable we want to retrieve
814 public function __get($name)
822 return $this->db
->getDriver();
824 return $this->db
->getDriver()->getConnection();
826 return $this->db
->getPlatform();
828 return $this->last_query
;
830 return $this->type_db
;
835 * Global isset method
836 * Required for twig to access properties via __get
838 * @param string $name name of the variable we want to retrieve
842 public function __isset($name)
854 return property_exists($this, $name);
858 * Get database information
862 public function getInfos()
872 if ($this->isPostgres()) {
873 $infos['engine'] = 'PostgreSQL';
874 $sql = 'SHOW server_version';
875 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
877 $infos['version'] = $result['server_version'];
879 $sql = 'SELECT pg_database_size(\'' . NAME_DB
. '\')';
880 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
882 $infos['size'] = (string)round($result['pg_database_size'] / 1024 / 1024);
884 $sql = 'SELECT @@sql_mode as mode, @@version AS version, @@version_comment AS version_comment';
885 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
888 $infos['engine'] = $result['version_comment'];
889 $infos['version'] = $result['version'];
890 $infos['sql_mode'] = $result['mode'];
892 $size_sql = 'SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) AS dbsize' .
893 ' FROM information_schema.tables WHERE table_schema="' . NAME_DB
. '"';
894 $result = $this->db
->query($size_sql, Adapter
::QUERY_MODE_EXECUTE
)
897 $infos['size'] = $result['dbsize'];
904 * Handle sequence on PostgreSQL
906 * When inserting a value on a field with a sequence,
907 * this one is not incremented.
908 * This happens when installing system values (for status, titles, ...)
910 * @see https://bugs.galette.eu/issues/1158
911 * @see https://bugs.galette.eu/issues/1374
913 * @param string $table Table name
914 * @param integer $expected Expected value
918 public function handleSequence($table, $expected)
920 if ($this->isPostgres()) {
921 //check for Postgres sequence
922 //see https://bugs.galette.eu/issues/1158
923 //see https://bugs.galette.eu/issues/1374
924 $seq = $table . '_id_seq';
926 $select = $this->select($seq);
927 $select->columns(['last_value']);
928 $results = $this->execute($select);
929 $result = $results->current();
930 if ($result->last_value
< $expected) {
932 'SELECT setval(\'' . PREFIX_DB
. $seq . '\', ' . $expected . ')',
933 Adapter
::QUERY_MODE_EXECUTE
940 * Check if current exception is on a duplicate key
942 * @param Throwable $exception Exception to check
946 public function isDuplicateException($exception)
948 return $exception instanceof \PDOException
950 (!$this->isPostgres() && $exception->getCode() == 23000)
951 ||
($this->isPostgres() && $exception->getCode() == 23505)
959 * @param string $table Table name, without prefix
960 * @param boolean $maymiss Whether the table can be missing, defaults to false
964 public function drop($table, $maymiss = false)
966 $sql = 'DROP TABLE ';
967 if ($maymiss === true) {
968 $sql .= 'IF EXISTS ';
970 $sql .= PREFIX_DB
. $table;
973 \Laminas\Db\Adapter\Adapter
::QUERY_MODE_EXECUTE
978 * Log queries in specific file
980 * @param string $query Query to add in logs
984 protected function log($query)
986 if (GALETTE_MODE
== 'DEV' ||
defined('GALETTE_SQL_DEBUG')) {
987 $logfile = GALETTE_LOGS_PATH
. 'galette_sql.log';
988 file_put_contents($logfile, $query . "\n", FILE_APPEND
);
993 * Get last generated value
995 * @param object $entity Entity instance
999 public function getLastGeneratedValue($entity): int
1001 /** @phpstan-ignore-next-line */
1002 return (int)$this->driver
->getLastGeneratedValue(
1003 $this->isPostgres() ?
1004 PREFIX_DB
. $entity::TABLE
. '_id_seq'
1010 * Get MySQL warnings
1014 public function getWarnings(): array
1016 $results = $this->db
->query('SHOW WARNINGS', Adapter
::QUERY_MODE_EXECUTE
);
1019 foreach ($results as $result) {
1020 $warnings[] = $result;