Add Mysql/MariaDB support

This commit is contained in:
Frédéric Guillot 2014-03-30 19:53:59 -04:00
parent 34711f5846
commit d9dfd9d619
19 changed files with 405 additions and 46 deletions

2
.gitignore vendored
View File

@ -42,8 +42,8 @@ Thumbs.db
# Vagrant #
###########
.vagrant
# App specific #
################
config.php

View File

@ -66,6 +66,7 @@ Documentation
- [Installation on Debian](docs/debian-installation.markdown)
- [Upgrade Kanboard to a new version](docs/update.markdown)
- [Sqlite database management](docs/sqlite-database.markdown)
- [How to use Mysql instead of Sqlite](docs/mysql-configuration.markdown)
- [How to use Kanboard with Vagrant](docs/vagrant.markdown)
- [Webhooks](docs/webhooks.markdown)

View File

@ -19,9 +19,9 @@ if (version_compare(PHP_VERSION, '5.4.0', '<')) {
}
}
// Check extension: PDO Sqlite
if (! extension_loaded('pdo_sqlite')) {
die('PHP extension required: pdo_sqlite');
// Check extension: PDO
if (! extension_loaded('pdo_sqlite') && ! extension_loaded('pdo_mysql')) {
die('PHP extension required: pdo_sqlite or pdo_mysql');
}
// Check extension: mbstring

View File

@ -10,12 +10,32 @@ $registry->db_version = 10;
$registry->db = function() use ($registry) {
require __DIR__.'/vendor/PicoDb/Database.php';
require __DIR__.'/models/schema.php';
$db = new \PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME
));
if (DB_DRIVER === 'sqlite') {
require __DIR__.'/schemas/sqlite.php';
$db = new \PicoDb\Database(array(
'driver' => 'sqlite',
'filename' => DB_FILENAME
));
}
elseif (DB_DRIVER === 'mysql') {
require __DIR__.'/schemas/mysql.php';
$db = new \PicoDb\Database(array(
'driver' => 'mysql',
'hostname' => DB_HOSTNAME,
'username' => DB_USERNAME,
'password' => DB_PASSWORD,
'database' => DB_NAME,
'charset' => 'utf8',
));
}
else {
die('Database driver not supported');
}
if ($db->schema()->check($registry->db_version)) {
return $db;
@ -83,8 +103,17 @@ defined('AUTO_REFRESH_DURATION') or define('AUTO_REFRESH_DURATION', 60);
// Custom session save path
defined('SESSION_SAVE_PATH') or define('SESSION_SAVE_PATH', '');
// Database filename
defined('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite');
// Application version
defined('APP_VERSION') or define('APP_VERSION', 'master');
// Database driver: sqlite or mysql
defined('DB_DRIVER') or define('DB_DRIVER', 'sqlite');
// Sqlite configuration
defined('DB_FILENAME') or define('DB_FILENAME', 'data/db.sqlite');
// Mysql configuration
defined('DB_USERNAME') or define('DB_USERNAME', 'root');
defined('DB_PASSWORD') or define('DB_PASSWORD', '');
defined('DB_HOSTNAME') or define('DB_HOSTNAME', 'localhost');
defined('DB_NAME') or define('DB_NAME', 'kanboard');

View File

@ -186,6 +186,11 @@ class Task extends Base
if (! empty($task['date_due'])) {
$task['date_due'] = date(t('m/d/Y'), $task['date_due']);
}
else {
$task['date_due'] = '';
}
$task['score'] = $task['score'] ?: '';
$this->response->html($this->template->layout('task_edit', array(
'errors' => array(),
@ -324,6 +329,15 @@ class Task extends Base
if (! $task) $this->notfound();
$this->checkProjectPermissions($task['project_id']);
if (! empty($task['date_due'])) {
$task['date_due'] = date(t('m/d/Y'), $task['date_due']);
}
else {
$task['date_due'] = '';
}
$task['score'] = $task['score'] ?: '';
$this->response->html($this->template->layout('task_new', array(
'errors' => array(),
'values' => $task,

View File

@ -0,0 +1,55 @@
How to use Mysql or MariaDB instead of Sqlite
=============================================
By default Kanboard use Sqlite to stores its data.
However it's possible to use Mysql or MariaDB instead of Sqlite.
By example, it can be useful if you don't want to store any data on the web server itself.
Mysql configuration
-------------------
### Create a database
The first step is to create a database on your Mysql server.
By example, you can do that with the command line mysql client:
```sql
CREATE DATABASE kanboard;
```
### Create a config file
All application constants can be overrided by using a config file at the root of the project.
The second step is to create a config file named `config.php`:
```
.
├── LICENSE
├── common.php
├── ....
├── config.php <== Our config file
├── ....
├── index.php
├── ....
└── vendor
```
### Define Mysql parameters
Inside our config file write those lines:
```php
<?php
// We choose to use Mysql instead of Sqlite
define('DB_DRIVER', 'mysql');
// Mysql parameters
define('DB_USERNAME', 'REPLACE_ME');
define('DB_PASSWORD', 'REPLACE_ME');
define('DB_HOSTNAME', 'REPLACE_ME');
define('DB_NAME', 'kanboard');
```
Now, you are ready to use Mysql!

View File

@ -32,22 +32,20 @@ class Board extends Base
*/
public function saveTasksPosition(array $values)
{
$this->db->startTransaction();
$results = array();
$taskModel = new Task($this->db, $this->event);
$this->db->startTransaction();
foreach ($values as $value) {
$results[] = $taskModel->move(
$value['task_id'],
$value['column_id'],
$value['position']
);
if (! $taskModel->move($value['task_id'], $value['column_id'], $value['position'])) {
$this->db->cancelTransaction();
return false;
}
}
$this->db->closeTransaction();
return ! in_array(false, $results, true);
return true;
}
/**

View File

@ -348,7 +348,15 @@ class Task extends Base
}
$original_task = $this->getById($values['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($values);
if ($original_task === false) {
return false;
}
$updated_task = $values;
unset($updated_task['id']);
$result = $this->db->table(self::TABLE)->eq('id', $values['id'])->update($updated_task);
// Trigger events
if ($result) {

1
schemas/.htaccess Normal file
View File

@ -0,0 +1 @@
Deny from all

165
schemas/mysql.php Normal file
View File

@ -0,0 +1,165 @@
<?php
namespace Schema;
function version_10($pdo)
{
}
function version_9($pdo)
{
}
function version_8($pdo)
{
}
function version_7($pdo)
{
}
function version_6($pdo)
{
}
function version_5($pdo)
{
}
function version_4($pdo)
{
}
function version_3($pdo)
{
}
function version_2($pdo)
{
}
function version_1($pdo)
{
$pdo->exec("
CREATE TABLE config (
language CHAR(5) DEFAULT 'en_US',
webhooks_token VARCHAR(255),
timezone VARCHAR(50) DEFAULT 'UTC'
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE users (
id INT NOT NULL AUTO_INCREMENT,
username VARCHAR(50),
password VARCHAR(255),
is_admin TINYINT DEFAULT 0,
default_project_id INT DEFAULT 0,
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE projects (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) UNIQUE,
is_active TINYINT DEFAULT 1,
token VARCHAR(255),
PRIMARY KEY (id)
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE project_has_users (
id INT NOT NULL AUTO_INCREMENT,
project_id INT,
user_id INT,
PRIMARY KEY (id),
UNIQUE KEY `idx_project_user` (project_id, user_id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE columns (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
position INT NOT NULL,
project_id INT NOT NULL,
task_limit INT DEFAULT '0',
UNIQUE KEY `idx_title_project` (title, project_id),
PRIMARY KEY (id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE tasks (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(255),
description TEXT,
date_creation INT,
date_completed INT,
date_due INT,
color_id VARCHAR(50),
project_id INT,
column_id INT,
owner_id INT DEFAULT '0',
position INT,
score INT,
is_active TINYINT DEFAULT 1,
PRIMARY KEY (id),
INDEX `idx_task_active` (is_active),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE,
FOREIGN KEY(column_id) REFERENCES columns(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE comments (
id INT NOT NULL AUTO_INCREMENT,
task_id INT,
user_id INT,
date INT,
comment TEXT,
PRIMARY KEY (id),
FOREIGN KEY(task_id) REFERENCES tasks(id) ON DELETE CASCADE,
FOREIGN KEY(user_id) REFERENCES tasks(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE actions (
id INT NOT NULL AUTO_INCREMENT,
project_id INT,
event_name VARCHAR(50),
action_name VARCHAR(50),
PRIMARY KEY (id),
FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
CREATE TABLE action_has_params (
id INT NOT NULL AUTO_INCREMENT,
action_id INT,
name VARCHAR(50),
value VARCHAR(50),
PRIMARY KEY (id),
FOREIGN KEY(action_id) REFERENCES actions(id) ON DELETE CASCADE
) ENGINE=InnoDB CHARSET=utf8
");
$pdo->exec("
INSERT INTO users
(username, password, is_admin)
VALUES ('admin', '".\password_hash('admin', PASSWORD_BCRYPT)."', '1')
");
$pdo->exec("
INSERT INTO config
(webhooks_token)
VALUES ('".\Model\Base::generateToken()."')
");
}

View File

@ -113,7 +113,7 @@ function version_1($pdo)
username TEXT,
password TEXT,
is_admin INTEGER DEFAULT 0,
default_project_id DEFAULT 0
default_project_id INTEGER DEFAULT 0
)
");

View File

@ -27,18 +27,20 @@
<?= t('Webhooks token:') ?>
<strong><?= Helper\escape($values['webhooks_token']) ?></strong>
</li>
<li>
<?= t('Database size:') ?>
<strong><?= Helper\format_bytes($db_size) ?></strong>
</li>
<li>
<a href="?controller=config&amp;action=downloadDb"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
<a href="?controller=config&amp;action=optimizeDb"><?= t('Optimize the database') ?></a>
<?= t('(VACUUM command)') ?>
</li>
<?php if (DB_DRIVER === 'sqlite'): ?>
<li>
<?= t('Database size:') ?>
<strong><?= Helper\format_bytes($db_size) ?></strong>
</li>
<li>
<a href="?controller=config&amp;action=downloadDb"><?= t('Download the database') ?></a>
<?= t('(Gzip compressed Sqlite file)') ?>
</li>
<li>
<a href="?controller=config&amp;action=optimizeDb"><?= t('Optimize the database') ?></a>
<?= t('(VACUUM command)') ?>
</li>
<?php endif ?>
<li>
<?= t('Official website:') ?>
<a href="http://kanboard.net/" target="_blank" rel="noreferer">http://kanboard.net/</a>

View File

@ -3,7 +3,7 @@
require_once __DIR__.'/../vendor/PicoDb/Database.php';
require_once __DIR__.'/../core/event.php';
require_once __DIR__.'/../core/translator.php';
require_once __DIR__.'/../models/schema.php';
require_once __DIR__.'/../schemas/sqlite.php';
require_once __DIR__.'/../models/task.php';
require_once __DIR__.'/../models/acl.php';
require_once __DIR__.'/../models/comment.php';

View File

@ -63,8 +63,6 @@ class CommentTest extends Base
$comment = $c->getById(1);
$this->assertNotEmpty($comment);
$this->assertEquals('bla', $comment['comment']);
$this->assertFalse($c->update(array('id' => 4, 'comment' => 'bla')));
}
public function testValidateCreation()

View File

@ -19,7 +19,12 @@ class Database
case 'sqlite':
require_once __DIR__.'/Drivers/Sqlite.php';
$this->pdo = new Sqlite($settings['filename']);
$this->pdo = new Sqlite($settings);
break;
case 'mysql':
require_once __DIR__.'/Drivers/Mysql.php';
$this->pdo = new Mysql($settings);
break;
default:

76
vendor/PicoDb/Drivers/Mysql.php vendored Normal file
View File

@ -0,0 +1,76 @@
<?php
namespace PicoDb;
class Mysql extends \PDO {
private $schema_table = 'schema_version';
public function __construct(array $settings)
{
$required_atttributes = array(
'hostname',
'username',
'password',
'database',
'charset',
);
foreach ($required_atttributes as $attribute) {
if (! isset($settings[$attribute])) {
throw new \LogicException('This configuration parameter is missing: "'.$attribute.'"');
}
}
$dsn = 'mysql:host='.$settings['hostname'].';dbname='.$settings['database'];
$options = array(
\PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES '.$settings['charset']
);
parent::__construct($dsn, $settings['username'], $settings['password'], $options);
if (isset($settings['schema_table'])) {
$this->schema_table = $settings['schema_table'];
}
}
public function getSchemaVersion()
{
$this->exec("CREATE TABLE IF NOT EXISTS `".$this->schema_table."` (`version` INT DEFAULT '0')");
$rq = $this->prepare('SELECT `version` FROM `'.$this->schema_table.'`');
$rq->execute();
$result = $rq->fetch(\PDO::FETCH_ASSOC);
if (isset($result['version'])) {
return (int) $result['version'];
}
else {
$this->exec('INSERT INTO `'.$this->schema_table.'` VALUES(0)');
}
return 0;
}
public function setSchemaVersion($version)
{
$rq = $this->prepare('UPDATE `'.$this->schema_table.'` SET `version`=?');
$rq->execute(array($version));
}
public function getLastId()
{
return $this->lastInsertId();
}
public function escapeIdentifier($value)
{
if (strpos($value, '.') !== false) return $value;
return '`'.$value.'`';
}
}

View File

@ -5,9 +5,19 @@ namespace PicoDb;
class Sqlite extends \PDO {
public function __construct($filename)
public function __construct(array $settings)
{
parent::__construct('sqlite:'.$filename);
$required_atttributes = array(
'filename',
);
foreach ($required_atttributes as $attribute) {
if (! isset($settings[$attribute])) {
throw new \LogicException('This configuration parameter is missing: "'.$attribute.'"');
}
}
parent::__construct('sqlite:'.$settings['filename']);
$this->exec('PRAGMA foreign_keys = ON');
}
@ -20,8 +30,7 @@ class Sqlite extends \PDO {
$result = $rq->fetch(\PDO::FETCH_ASSOC);
if (isset($result['user_version'])) {
return $result['user_version'];
return (int) $result['user_version'];
}
return 0;

View File

@ -18,7 +18,6 @@ class Schema
$current_version = $this->db->getConnection()->getSchemaVersion();
if ($current_version < $last_version) {
return $this->migrateTo($current_version, $last_version);
}

View File

@ -67,8 +67,7 @@ class Table
$result = $this->db->execute($sql, $values);
if ($result !== false && $result->rowCount() > 0) {
if ($result !== false/* && $result->rowCount() > 0*/) {
return true;
}