3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
10 * Copyright © 2011-2014 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-2014 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
;
40 use Laminas\Db\Adapter\Adapter
;
41 use Laminas\Db\Adapter\Driver\DriverInterface
;
42 use Laminas\Db\Adapter\Driver\ConnectionInterface
;
43 use Laminas\Db\Adapter\Platform\PlatformInterface
;
44 use Laminas\Db\Sql\Insert
;
45 use Laminas\Db\Sql\Select
;
46 use Laminas\Db\Sql\Sql
;
54 * @author Johan Cwiklinski <johan@x-tnd.be>
55 * @copyright 2011-2014 The Galette Team
56 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
57 * @link http://framework.zend.com/apidoc/2.2/namespaces/Zend.Db.html
58 * @since Available since 0.7dev - 2011-07-27
60 * @property Adapter $db
62 * @property DriverInterface $driver
63 * @property ConnectionInterface $connection
64 * @property PlatformInterface $platform
65 * @property string $query_string
66 * @property string $type_db
81 public const MYSQL
= 'mysql';
82 public const PGSQL
= 'pgsql';
84 public const MYSQL_DEFAULT_PORT
= 3306;
85 public const PGSQL_DEFAULT_PORT
= 5432;
90 * @param array $dsn Connection information
91 * If not set, database constants will be used.
93 public function __construct($dsn = null)
97 if ($dsn !== null && is_array($dsn)) {
98 $_type_db = $dsn['TYPE_DB'];
99 $_host_db = $dsn['HOST_DB'];
100 $_port_db = $dsn['PORT_DB'];
101 $_user_db = $dsn['USER_DB'];
102 $_pwd_db = $dsn['PWD_DB'];
103 $_name_db = $dsn['NAME_DB'];
114 if ($_type_db === self
::MYSQL
) {
115 $_type = 'Pdo_Mysql';
116 } elseif ($_type_db === self
::PGSQL
) {
117 $_type = 'Pdo_Pgsql';
119 throw new \
Exception("Type $_type_db not known (dsn: $_user_db@$_host_db(:$_port_db)/$_name_db)");
122 $this->type_db
= $_type_db;
123 $this->options
= array(
125 'hostname' => $_host_db,
127 'username' => $_user_db,
128 'password' => $_pwd_db,
129 'database' => $_name_db
131 if ($_type_db === self
::MYSQL
&& !defined('NON_UTF_DBCONNECT')) {
132 $this->options
['charset'] = 'utf8';
135 $this->doConnection();
136 } catch (\Exception
$e) {
137 // perhaps factory() failed to load the specified Adapter class
139 '[Db] Error (' . $e->getCode() . '|' .
140 $e->getMessage() . ')',
148 * Do database connection
152 private function doConnection()
154 $this->db
= new Adapter($this->options
);
155 $this->db
->getDriver()->getConnection()->connect();
156 $this->sql
= new Sql($this->db
);
158 if (!$this->isPostgres()) {
159 $this->db
->query("SET GLOBAL sql_mode=(SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY',''));");
163 '[Db] Database connection was successfull!',
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 (\Exception
$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
279 * @return true|array true if connection was successfull,
280 * an array with some infos otherwise
282 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();
309 $_db = new Adapter($_options);
310 $_db->getDriver()->getConnection()->connect();
313 '[' . __METHOD__
. '] Database connection was successfull!',
317 } catch (\Exception
$e) {
318 // perhaps failed to load the specified Adapter class
320 '[' . __METHOD__
. '] Connection error (' . $e->getCode() . '|' .
321 $e->getMessage() . ')',
329 * Drop test table if it exists, so we can make all checks.
333 public function dropTestTable()
336 $this->db
->query('DROP TABLE IF EXISTS galette_test');
337 Analog
::log('Test table successfully dropped.', Analog
::DEBUG
);
338 } catch (\Exception
$e) {
340 'Cannot drop test table! ' . $e->getMessage(),
347 * Checks GRANT access for install time
349 * @param string $mode are we at install time (i) or update time (u) ?
351 * @return array containing each test. Each array entry could
352 * be either true or contains an exception of false if test did not
355 public function grantCheck($mode = 'i')
358 'Check for database rights (mode ' . $mode . ')',
371 $results['alter'] = false;
374 //can Galette CREATE tables?
376 $sql = 'CREATE TABLE galette_test (
377 test_id INTEGER NOT NULL,
378 test_text VARCHAR(20)
380 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
381 $results['create'] = true;
382 } catch (\Exception
$e) {
383 Analog
::log('Cannot CREATE TABLE', Analog
::WARNING
);
384 //if we cannot create tables, we cannot check other permissions
386 $results['create'] = $e;
389 //all those tests need the table to exists
392 //can Galette ALTER tables? (only for update mode)
394 $sql = 'ALTER TABLE galette_test ALTER test_text SET DEFAULT \'nothing\'';
395 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
396 $results['alter'] = true;
397 } catch (\Exception
$e) {
399 'Cannot ALTER TABLE | ' . $e->getMessage(),
402 $results['alter'] = $e;
406 //can Galette INSERT records ?
409 'test_text' => 'a simple text'
412 $insert = $this->sql
->insert('galette_test');
413 $insert->values($values);
415 $res = $this->execute($insert);
417 if ($res->count() === 1) {
418 $results['insert'] = true;
420 throw new \
Exception('No row inserted!');
422 } catch (\Exception
$e) {
424 'Cannot INSERT records | ' . $e->getMessage(),
427 //if we cannot insert records, some others tests cannot be done
429 $results['insert'] = $e;
432 //all those tests need that the first record exists
434 //can Galette UPDATE records ?
436 'test_text' => 'another simple text'
439 $update = $this->sql
->update('galette_test');
440 $update->set($values)->where(
441 array('test_id' => 1)
443 $res = $this->execute($update);
444 if ($res->count() === 1) {
445 $results['update'] = true;
447 throw new \
Exception('No row updated!');
449 } catch (\Exception
$e) {
451 'Cannot UPDATE records | ' . $e->getMessage(),
454 $results['update'] = $e;
457 //can Galette SELECT records ?
460 $select = $this->sql
->select('galette_test');
461 $select->where('test_id = 1');
462 $res = $this->execute($select);
463 $pass = $res->count() === 1;
466 $results['select'] = true;
468 throw new \
Exception('Select is empty!');
470 } catch (\Exception
$e) {
472 'Cannot SELECT records | ' . $e->getMessage(),
475 $results['select'] = $e;
478 //can Galette DELETE records ?
480 $delete = $this->sql
->delete('galette_test');
481 $delete->where(array('test_id' => 1));
482 $this->execute($delete);
483 $results['delete'] = true;
484 } catch (\Exception
$e) {
486 'Cannot DELETE records | ' . $e->getMessage(),
489 $results['delete'] = $e;
493 //can Galette DROP tables ?
495 $sql = 'DROP TABLE galette_test';
496 $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
);
497 $results['drop'] = true;
498 } catch (\Exception
$e) {
500 'Cannot DROP TABLE | ' . $e->getMessage(),
503 $results['drop'] = $e;
511 * Get a list of Galette's tables
513 * @param string $prefix Specified table prefix, PREFIX_DB if null
517 public function getTables($prefix = null)
519 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
520 $tmp_tables_list = $metadata->getTableNames();
522 if ($prefix === null) {
526 $tables_list = array();
527 //filter table_list: we only want PREFIX_DB tables
528 foreach ($tmp_tables_list as $t) {
529 if (preg_match('/^' . $prefix . '/', $t)) {
537 * Get columns for a specified table
539 * @param string $table Table name
543 public function getColumns($table)
545 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
546 $table = $metadata->getTable(PREFIX_DB
. $table);
547 return $table->getColumns();
551 * Converts recursively database to UTF-8
553 * @param string $prefix Specified table prefix
554 * @param boolean $content_only Proceed only content (no table conversion)
558 public function convertToUTF($prefix = null, $content_only = false)
560 if ($this->isPostgres()) {
562 'Cannot change encoding on PostgreSQL database',
567 if ($prefix === null) {
572 $this->connection
->beginTransaction();
574 $tables = $this->getTables($prefix);
576 foreach ($tables as $table) {
577 if ($content_only === false) {
578 //Change whole table charset
579 //CONVERT TO instruction will take care of each fields,
580 //but converting data stay our problem.
581 $query = 'ALTER TABLE ' . $table .
582 ' CONVERT TO CHARACTER SET utf8 COLLATE utf8_unicode_ci';
586 Adapter
::QUERY_MODE_EXECUTE
590 'Charset successfully changed for table `' . $table . '`',
596 if ($table != $prefix . 'pictures') {
597 $this->convertContentToUTF($prefix, $table);
600 $this->connection
->commit();
601 } catch (\Exception
$e) {
602 $this->connection
->rollBack();
604 'An error occurred while converting to utf table ' .
605 $table . ' (' . $e->getMessage() . ')',
612 * Converts dtabase content to UTF-8
614 * @param string $prefix Specified table prefix
615 * @param string $table the table we want to convert datas from
619 private function convertContentToUTF($prefix, $table)
623 $query = 'SET NAMES latin1';
626 Adapter
::QUERY_MODE_EXECUTE
628 } catch (\Exception
$e) {
630 'Cannot SET NAMES on table `' . $table . '`. ' .
637 $metadata = \Laminas\Db\Metadata\Source\Factory
::createSourceFromAdapter($this->db
);
638 $tbl = $metadata->getTable($table);
639 $columns = $tbl->getColumns();
640 $constraints = $tbl->getConstraints();
643 foreach ($constraints as $constraint) {
644 if ($constraint->getType() === 'PRIMARY KEY') {
645 $pkeys = $constraint->getColumns();
649 if (count($pkeys) == 0) {
650 //no primary key! How to do an update without that?
651 //Prior to 0.7, l10n and dynamic_fields tables does not
652 //contains any primary key. Since encoding conversion is done
653 //_before_ the SQL upgrade, we'll have to manually
655 if (preg_match('/' . $prefix . 'dynamic_fields/', $table) !== 0) {
662 } elseif (preg_match('/' . $prefix . 'l10n/', $table) !== 0) {
668 //not a know case, we do not perform any update.
669 throw new \
Exception(
670 'Cannot define primary key for table `' . $table .
676 $select = $this->sql
->select($table);
677 $results = $this->execute($select);
679 foreach ($results as $row) {
684 foreach ($pkeys as $k) {
685 $where[] = $k . ' = "' . $row->$k . '"';
689 foreach ($row as $key => $value) {
690 $data[$key] = $value;
693 //finally, update data!
694 $update = $this->sql
->update($table);
695 $update->set($data)->where($where);
696 $this->execute($update);
698 } catch (\Exception
$e) {
700 'An error occurred while converting contents to UTF-8 for table ' .
701 $table . ' (' . $e->getMessage() . ')',
708 * Is current database using Postgresql?
712 public function isPostgres()
714 return $this->type_db
=== self
::PGSQL
;
718 * Instanciate a select query
720 * @param string $table Table name, without prefix
721 * @param string $alias Tables alias, optionnal
725 public function select($table, $alias = null)
727 if ($alias === null) {
728 return $this->sql
->select(
732 return $this->sql
->select(
734 $alias => PREFIX_DB
. $table
741 * Instanciate an insert query
743 * @param string $table Table name, without prefix
747 public function insert($table)
749 return $this->sql
->insert(
755 * Instanciate an update query
757 * @param string $table Table name, without prefix
761 public function update($table)
763 return $this->sql
->update(
769 * Instanciate a delete query
771 * @param string $table Table name, without prefix
775 public function delete($table)
777 return $this->sql
->delete(
783 * Execute query string
785 * @param SqlInterface $sql SQL object
789 public function execute($sql)
792 $query_string = $this->sql
->buildSqlString($sql);
793 $this->last_query
= $query_string;
795 'Executing query: ' . $query_string,
798 return $this->db
->query(
800 Adapter
::QUERY_MODE_EXECUTE
802 } catch (\Exception
$e) {
803 $msg = 'Query error: ';
804 if (isset($query_string)) {
805 $msg .= $query_string;
808 $msg . ' ' . $e->__toString(),
811 if ($sql instanceof Insert
&& $this->isDuplicateException($e)) {
812 throw new \
OverflowException('Duplicate entry', 0, $e);
819 * Global getter method
821 * @param string $name name of the variable we want to retrieve
825 public function __get($name)
835 return $this->db
->getDriver();
838 return $this->db
->getDriver()->getConnection();
841 return $this->db
->getPlatform();
844 return $this->last_query
;
847 return $this->type_db
;
853 * Get database information
857 public function getInfos()
867 if ($this->isPostgres()) {
868 $infos['engine'] = 'PostgreSQL';
869 $sql = 'SHOW server_version';
870 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
872 $infos['version'] = $result['server_version'];
874 $sql = 'SELECT pg_database_size(\'' . NAME_DB
. '\')';
875 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
877 $infos['size'] = (string)round($result['pg_database_size'] / 1024 / 1024);
879 $sql = 'SELECT @@sql_mode as mode, @@version AS version, @@version_comment AS version_comment';
880 $result = $this->db
->query($sql, Adapter
::QUERY_MODE_EXECUTE
)
883 $infos['engine'] = $result['version_comment'];
884 $infos['version'] = $result['version'];
885 $infos['sql_mode'] = $result['mode'];
887 $size_sql = 'SELECT ROUND(SUM(data_length + index_length) / 1024 / 1024, 1) AS dbsize' .
888 ' FROM information_schema.tables WHERE table_schema="' . NAME_DB
. '"';
889 $result = $this->db
->query($size_sql, Adapter
::QUERY_MODE_EXECUTE
)
892 $infos['size'] = $result['dbsize'];
899 * Handle sequence on PostgreSQL
901 * When inserting a value on a field with a sequence,
902 * this one is not incremented.
903 * This happens when installing system values (for status, titles, ...)
905 * @see https://bugs.galette.eu/issues/1158
906 * @see https://bugs.galette.eu/issues/1374
908 * @param string $table Table name
909 * @param integer $expected Expected value
913 public function handleSequence($table, $expected)
915 if ($this->isPostgres()) {
916 //check for Postgres sequence
917 //see https://bugs.galette.eu/issues/1158
918 //see https://bugs.galette.eu/issues/1374
919 $seq = $table . '_id_seq';
921 $select = $this->select($seq);
922 $select->columns(['last_value']);
923 $results = $this->execute($select);
924 $result = $results->current();
925 if ($result->last_value
< $expected) {
927 'SELECT setval(\'' . PREFIX_DB
. $seq . '\', ' . $expected . ')',
928 Adapter
::QUERY_MODE_EXECUTE
935 * Check if current exception is on a duplicate key
937 * @param \Exception $exception Exception to check
941 public function isDuplicateException($exception)
943 return $exception instanceof \PDOException
945 (!$this->isPostgres() && $exception->getCode() == 23000)
946 ||
($this->isPostgres() && $exception->getCode() == 23505)