Switched to sqlite (closed #38)

This commit is contained in:
Marcin Kurczewski 2014-09-14 16:16:15 +02:00
parent d450f5794e
commit 4526345e5b
25 changed files with 303 additions and 134 deletions

View file

@ -1,6 +1,7 @@
{
"require": {
"mnapoli/php-di": "~4.0",
"jbrooksuk/phpcheckstyle": "dev-master"
"jbrooksuk/phpcheckstyle": "dev-master",
"lichtner/fluentpdo": "dev-master"
}
}

2
data/.gitignore vendored
View file

@ -1,2 +1,4 @@
db.sqlite
executed_upgrades.txt
local.ini
thumbnails

View file

@ -11,9 +11,7 @@ activationSubject = szuru2 - account activation
activationBodyPath = mail/activation.txt
[database]
host = localhost
port = 27017
name = booru-dev
dsn = sqlite:db.sqlite
[security]
secret = change

View file

@ -77,6 +77,10 @@ module.exports = function(grunt) {
tests: {
command: 'phpunit --strict --bootstrap src/AutoLoader.php tests/',
},
upgrade: {
command: 'php upgrade.php',
},
},
cssmin: {
@ -130,6 +134,8 @@ module.exports = function(grunt) {
grunt.registerTask('default', ['checkstyle', 'tests']);
grunt.registerTask('checkstyle', ['jshint', 'shell:phpcheckstyle']);
grunt.registerTask('tests', ['shell:tests']);
grunt.registerTask('update', ['shell:upgrade']);
grunt.registerTask('upgrade', ['shell:upgrade']);
grunt.registerTask('clean', function() {
fs.unlink('public_html/app.min.html');

View file

@ -3,25 +3,28 @@ namespace Szurubooru\Dao;
abstract class AbstractDao implements ICrudDao
{
protected $db;
protected $collection;
protected $pdo;
protected $fpdo;
protected $tableName;
protected $entityName;
protected $entityConverter;
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
$collectionName,
$tableName,
$entityName)
{
$this->entityConverter = new EntityConverter($entityName);
$this->db = $databaseConnection->getDatabase();
$this->collection = $this->db->selectCollection($collectionName);
$this->entityName = $entityName;
$this->tableName = $tableName;
$this->pdo = $databaseConnection->getPDO();
$this->fpdo = new \FluentPDO($this->pdo);
}
public function getCollection()
public function getTableName()
{
return $this->collection;
return $this->tableName;
}
public function getEntityConverter()
@ -34,14 +37,12 @@ abstract class AbstractDao implements ICrudDao
$arrayEntity = $this->entityConverter->toArray($entity);
if ($entity->getId())
{
$savedId = $arrayEntity['_id'];
unset($arrayEntity['_id']);
$this->collection->update(['_id' => new \MongoId($entity->getId())], $arrayEntity, ['w' => true]);
$arrayEntity['_id'] = $savedId;
$this->fpdo->update($this->tableName)->set($arrayEntity)->where('id', $entity->getId())->execute();
}
else
{
$this->collection->insert($arrayEntity, ['w' => true]);
$this->fpdo->insertInto($this->tableName)->values($arrayEntity)->execute();
$arrayEntity['id'] = $this->pdo->lastInsertId();
}
$entity = $this->entityConverter->toEntity($arrayEntity);
return $entity;
@ -50,27 +51,43 @@ abstract class AbstractDao implements ICrudDao
public function findAll()
{
$entities = [];
foreach ($this->collection->find() as $key => $arrayEntity)
$query = $this->fpdo->from($this->tableName);
foreach ($query as $arrayEntity)
{
$entity = $this->entityConverter->toEntity($arrayEntity);
$entities[$key] = $entity;
$entities[$entity->getId()] = $entity;
}
return $entities;
}
public function findById($entityId)
{
$arrayEntity = $this->collection->findOne(['_id' => new \MongoId($entityId)]);
return $this->entityConverter->toEntity($arrayEntity);
return $this->findOneBy('id', $entityId);
}
public function deleteAll()
{
$this->collection->remove();
$this->fpdo->deleteFrom($this->tableName)->execute();
}
public function deleteById($entityId)
{
$this->collection->remove(['_id' => new \MongoId($entityId)]);
return $this->deleteBy('id', $entityId);
}
protected function hasAnyRecords()
{
return count(iterator_to_array($this->fpdo->from($this->tableName)->limit(1))) > 0;
}
protected function findOneBy($columnName, $value)
{
$arrayEntity = iterator_to_array($this->fpdo->from($this->tableName)->where($columnName, $value));
return $arrayEntity ? $this->entityConverter->toEntity($arrayEntity[0]) : null;
}
protected function deleteBy($columnName, $value)
{
$this->fpdo->deleteFrom($this->tableName)->where($columnName, $value)->execute();
}
}

View file

@ -20,11 +20,6 @@ final class EntityConverter
$reflectionProperty->setAccessible(true);
$arrayEntity[$reflectionProperty->getName()] = $reflectionProperty->getValue($entity);
}
if ($entity->getId())
{
$arrayEntity['_id'] = $arrayEntity['id'];
unset($arrayEntity['id']);
}
return $arrayEntity;
}
@ -46,12 +41,6 @@ final class EntityConverter
}
}
$reflectionProperty = $reflectionClass->getProperty('id');
$reflectionProperty->setAccessible(true);
$reflectionProperty->setValue($entity, isset($arrayEntity['_id'])
? (string) $arrayEntity['_id']
: null);
return $entity;
}
}

View file

@ -6,14 +6,17 @@ abstract class AbstractSearchService
const ORDER_DESC = -1;
const ORDER_ASC = 1;
private $collection;
private $tableName;
private $entityConverter;
private $fpdo;
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\AbstractDao $dao)
{
$this->collection = $dao->getCollection();
$this->tableName = $dao->getTableName();
$this->entityConverter = $dao->getEntityConverter();
$this->fpdo = new \FluentPDO($databaseConnection->getPDO());
}
public function getFiltered(
@ -31,16 +34,26 @@ abstract class AbstractSearchService
$pageSize = min(100, max(1, $searchFilter->pageSize));
$pageNumber = max(1, $searchFilter->pageNumber) - 1;
$cursor = $this->collection->find($filter);
$totalRecords = $cursor->count();
$cursor->sort($order);
$cursor->skip($pageSize * $pageNumber);
$cursor->limit($pageSize);
//todo: clean up
$orderByString = '';
foreach ($order as $orderColumn => $orderDir)
{
$orderByString .= $orderColumn . ' ' . ($orderDir === self::ORDER_DESC ? 'DESC' : 'ASC') . ', ';
}
$orderByString = substr($orderByString, 0, -2);
$query = $this->fpdo
->from($this->tableName)
->orderBy($orderByString)
->limit($pageSize)
->offset($pageSize * $pageNumber);
$entities = [];
foreach ($cursor as $arrayEntity)
foreach ($query as $arrayEntity)
$entities[] = $this->entityConverter->toEntity($arrayEntity);
$query->select('COUNT(1) AS c');
$totalRecords = intval(iterator_to_array($query)[0]['c']);
return new \Szurubooru\Dao\SearchResult($searchFilter, $entities, $totalRecords);
}
@ -61,7 +74,7 @@ abstract class AbstractSearchService
protected function getDefaultOrderColumn()
{
return '_id';
return 'id';
}
protected function getDefaultOrderDir()

View file

@ -3,9 +3,11 @@ namespace Szurubooru\Dao\Services;
class UserSearchService extends AbstractSearchService
{
public function __construct(\Szurubooru\Dao\UserDao $userDao)
public function __construct(
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Dao\UserDao $userDao)
{
parent::__construct($userDao);
parent::__construct($databaseConnection, $userDao);
}
protected function getOrderColumn($token)

View file

@ -10,17 +10,16 @@ class TokenDao extends AbstractDao
public function findByName($tokenName)
{
$arrayEntity = $this->collection->findOne(['name' => $tokenName]);
return $this->entityConverter->toEntity($arrayEntity);
return $this->findOneBy('name', $tokenName);
}
public function deleteByName($tokenName)
{
$this->collection->remove(['name' => $tokenName]);
return $this->deleteBy('name', $tokenName);
}
public function deleteByAdditionalData($additionalData)
{
$this->collection->remove(['additionalData' => $additionalData]);
return $this->deleteBy('additionalData', $additionalData);
}
}

View file

@ -11,27 +11,27 @@ class UserDao extends AbstractDao implements ICrudDao
public function findByName($userName)
{
$arrayEntity = $this->collection->findOne(['name' => $userName]);
return $this->entityConverter->toEntity($arrayEntity);
return $this->findOneBy('name', $userName);
}
public function findByEmail($userEmail, $allowUnconfirmed = false)
{
$arrayEntity = $this->collection->findOne(['email' => $userEmail]);
if (!$arrayEntity and $allowUnconfirmed)
$arrayEntity = $this->collection->findOne(['emailUnconfirmed' => $userEmail]);
return $this->entityConverter->toEntity($arrayEntity);
$result = $this->findOneBy('email', $userEmail);
if (!$result and $allowUnconfirmed)
{
$result = $this->findOneBy('emailUnconfirmed', $userEmail);
}
return $result;
}
public function hasAnyUsers()
{
return (bool) $this->collection->findOne();
return $this->hasAnyRecords();
}
public function deleteByName($userName)
{
$this->collection->remove(['name' => $userName]);
$tokens = $this->db->selectCollection('tokens');
$tokens->remove(['additionalData' => $userName]);
$this->deleteBy('name', $userName);
$this->fpdo->deleteFrom('tokens')->where('additionalData', $userName);
}
}

View file

@ -3,32 +3,35 @@ namespace Szurubooru;
final class DatabaseConnection
{
private $database;
private $connection;
private $pdo;
private $config;
public function __construct(\Szurubooru\Config $config)
{
$connectionString = $this->getConnectionString($config);
$this->connection = new \MongoClient($connectionString);
$this->database = $this->connection->selectDb($config->database->name);
$this->config = $config;
}
public function getConnection()
public function getPDO()
{
return $this->connection;
if (!$this->pdo)
{
$this->createPDO();
}
return $this->pdo;
}
public function getDatabase()
public function close()
{
return $this->database;
$this->pdo = null;
}
private function getConnectionString(\Szurubooru\Config $config)
private function createPDO()
{
return sprintf(
'mongodb://%s:%d/%s',
$config->database->host,
$config->database->port,
$config->database->name);
$cwd = getcwd();
if ($this->config->getDataDirectory())
chdir($this->config->getDataDirectory());
$this->pdo = new \PDO($this->config->database->dsn);
$this->pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
chdir($cwd);
}
}

View file

@ -3,9 +3,9 @@ namespace Szurubooru\Entities;
final class Token extends Entity
{
const PURPOSE_LOGIN = 'login';
const PURPOSE_ACTIVATE = 'activate';
const PURPOSE_PASSWORD_RESET = 'passwordReset';
const PURPOSE_LOGIN = 0;
const PURPOSE_ACTIVATE = 1;
const PURPOSE_PASSWORD_RESET = 2;
protected $name;
protected $purpose;

View file

@ -0,0 +1,75 @@
<?php
namespace Szurubooru\Services;
final class UpgradeService
{
private $config;
private $upgrades;
private $databaseConnection;
private $executedUpgradeNames = [];
public function __construct(
\Szurubooru\Config $config,
\Szurubooru\DatabaseConnection $databaseConnection,
\Szurubooru\Upgrades\UpgradeRepository $upgradeRepository)
{
$this->config = $config;
$this->databaseConnection = $databaseConnection;
$this->upgrades = $upgradeRepository->getUpgrades();
$this->loadExecutedUpgradeNames();
}
public function runUpgradesVerbose()
{
$this->runUpgrades(true);
}
public function runUpgradesQuiet()
{
$this->runUpgrades(false);
}
private function runUpgrades($verbose)
{
foreach ($this->upgrades as $upgrade)
{
if ($this->isUpgradeNeeded($upgrade))
{
if ($verbose)
echo 'Running ' . get_class($upgrade) . PHP_EOL;
$this->runUpgrade($upgrade);
}
}
}
private function isUpgradeNeeded(\Szurubooru\Upgrades\IUpgrade $upgrade)
{
return !in_array(get_class($upgrade), $this->executedUpgradeNames);
}
private function runUpgrade(\Szurubooru\Upgrades\IUpgrade $upgrade)
{
$upgrade->run($this->databaseConnection);
$this->executedUpgradeNames[] = get_class($upgrade);
$this->saveExecutedUpgradeNames();
}
private function loadExecutedUpgradeNames()
{
$infoFilePath = $this->getExecutedUpgradeNamesFilePath();
if (!file_exists($infoFilePath))
return;
$this->executedUpgradeNames = explode("\n", file_get_contents($infoFilePath));
}
private function saveExecutedUpgradeNames()
{
$infoFilePath = $this->getExecutedUpgradeNamesFilePath();
file_put_contents($infoFilePath, implode("\n", $this->executedUpgradeNames));
}
private function getExecutedUpgradeNamesFilePath()
{
return $this->config->getDataDirectory() . DIRECTORY_SEPARATOR . 'executed_upgrades.txt';
}
}

View file

@ -1,29 +0,0 @@
<?php
namespace Szurubooru;
final class UpgradeService
{
private $db;
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
{
$this->db = $databaseConnection->getDatabase();
}
public function prepareForUsage()
{
$this->db->createCollection('posts');
}
public function removeAllData()
{
foreach ($this->db->getCollectionNames() as $collectionName)
$this->removeCollectionData($collectionName);
}
private function removeCollectionData($collectionName)
{
$this->db->$collectionName->remove();
$this->db->$collectionName->deleteIndexes();
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Szurubooru\Upgrades;
interface IUpgrade
{
public function run(\Szurubooru\DatabaseConnection $databaseConnection);
}

View file

@ -0,0 +1,40 @@
<?php
namespace Szurubooru\Upgrades;
class Upgrade01 implements IUpgrade
{
public function run(\Szurubooru\DatabaseConnection $databaseConnection)
{
$databaseConnection->getPDO()->exec('
CREATE TABLE "users"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
passwordHash TEXT NOT NULL,
email TEXT,
emailUnconfirmed TEXT,
accessRank INTEGER NOT NULL,
browsingSettings TEXT,
banned INTEGER,
registrationTime INTEGER DEFAULT NULL,
lastLoginTime INTEGER DEFAULT NULL,
avatarStyle INTEGER DEFAULT 1
);');
$databaseConnection->getPDO()->exec('
CREATE TABLE "tokens"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
purpose INTEGER NOT NULL,
additionalData TEXT
);');
$databaseConnection->getPDO()->exec('
CREATE TABLE "posts"
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL
);');
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Szurubooru\Upgrades;
class UpgradeRepository
{
private $upgrades = [];
public function __construct(array $upgrades)
{
$this->upgrades = $upgrades;
}
public function getUpgrades()
{
return $this->upgrades;
}
}

View file

@ -5,6 +5,13 @@ return [
\Szurubooru\Config::class => DI\object()->constructor($dataDirectory),
\Szurubooru\ControllerRepository::class => DI\object()->constructor(DI\link('controllers')),
\Szurubooru\Upgrades\UpgradeRepository::class => DI\object()->constructor(DI\link('upgrades')),
'upgrades' => DI\factory(function (DI\container $container) {
return [
$container->get(\Szurubooru\Upgrades\Upgrade01::class),
];
}),
'controllers' => DI\factory(function (DI\container $container) {
return [

View file

@ -4,24 +4,24 @@ namespace Szurubooru\Tests;
abstract class AbstractDatabaseTestCase extends \Szurubooru\Tests\AbstractTestCase
{
protected $databaseConnection;
protected $upgradeService;
public function setUp()
{
$host = 'localhost';
$port = 27017;
$database = 'test';
$config = $this->mockConfig();
$config->set('database/host', 'localhost');
$config->set('database/port', '27017');
$config->set('database/name', 'test');
parent::setUp();
$config = $this->mockConfig($this->createTestDirectory());
$config->set('database/dsn', 'sqlite::memory:');
$this->databaseConnection = new \Szurubooru\DatabaseConnection($config);
$this->upgradeService = new \Szurubooru\UpgradeService($this->databaseConnection);
$this->upgradeService->prepareForUsage();
$upgradeRepository = \Szurubooru\Injector::get(\Szurubooru\Upgrades\UpgradeRepository::class);
$upgradeService = new \Szurubooru\Services\UpgradeService($config, $this->databaseConnection, $upgradeRepository);
$upgradeService->runUpgradesQuiet();
}
public function tearDown()
{
$this->upgradeService->removeAllData();
parent::tearDown();
if ($this->databaseConnection)
$this->databaseConnection->close();
}
}

View file

@ -54,4 +54,9 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
}
}
require_once __DIR__
. DIRECTORY_SEPARATOR . '..'
. DIRECTORY_SEPARATOR . 'vendor'
. DIRECTORY_SEPARATOR . 'autoload.php';
date_default_timezone_set('UTC');

View file

@ -1,11 +1,6 @@
<?php
namespace Szurubooru\Tests;
require_once __DIR__
. DIRECTORY_SEPARATOR . '..'
. DIRECTORY_SEPARATOR . 'vendor'
. DIRECTORY_SEPARATOR . 'autoload.php';
class ControllerRepositoryTest extends \Szurubooru\Tests\AbstractTestCase
{
public function testInjection()

View file

@ -23,11 +23,9 @@ class UserSearchServiceTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testSorting()
{
$user1 = new \Szurubooru\Entities\User();
$user1->setName('reginald');
$user1 = $this->getTestUser('reginald');
$user2 = $this->getTestUser('beartato');
$user1->setRegistrationTime(date('c', mktime(3, 2, 1)));
$user2 = new \Szurubooru\Entities\User();
$user2->setName('beartato');
$user2->setRegistrationTime(date('c', mktime(1, 2, 3)));
$this->userDao->save($user1);
@ -62,6 +60,17 @@ class UserSearchServiceTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
private function getUserSearchService()
{
return new \Szurubooru\Dao\Services\UserSearchService($this->userDao);
return new \Szurubooru\Dao\Services\UserSearchService($this->databaseConnection, $this->userDao);
}
private function getTestUser($userName)
{
$user = new \Szurubooru\Entities\User();
$user->setName($userName);
$user->setPasswordHash('whatever');
$user->setLastLoginTime('whatever');
$user->setRegistrationTime('whatever');
$user->setAccessRank('whatever');
return $user;
}
}

View file

@ -9,6 +9,7 @@ final class TokenDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
$token = new \Szurubooru\Entities\Token();
$token->setName('test');
$token->setPurpose(\Szurubooru\Entities\Token::PURPOSE_LOGIN);
$tokenDao->save($token);
$expected = $token;

View file

@ -7,13 +7,11 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
$userDao = $this->getUserDao();
$user = new \Szurubooru\Entities\User();
$user->setName('test');
$user = $this->getTestUser();
$userDao->save($user);
$expected = $user;
$actual = $userDao->findByName($user->getName());
$this->assertEquals($actual, $expected);
}
@ -29,13 +27,10 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
public function testCheckingUserPresence()
{
$userDao = $this->getUserDao();
$this->assertFalse($userDao->hasAnyUsers());
$user = new \Szurubooru\Entities\User();
$user->setName('test');
$user = $this->getTestUser();
$userDao->save($user);
$this->assertTrue($userDao->hasAnyUsers());
}
@ -43,4 +38,15 @@ final class UserDaoTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
return new \Szurubooru\Dao\UserDao($this->databaseConnection);
}
private function getTestUser()
{
$user = new \Szurubooru\Entities\User();
$user->setName('test');
$user->setPasswordHash('whatever');
$user->setLastLoginTime('whatever');
$user->setRegistrationTime('whatever');
$user->setAccessRank('whatever');
return $user;
}
}

6
upgrade.php Normal file
View file

@ -0,0 +1,6 @@
<?php
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . 'autoload.php');
require_once(__DIR__ . DIRECTORY_SEPARATOR . 'src' . DIRECTORY_SEPARATOR . 'AutoLoader.php');
$upgradeService = Szurubooru\Injector::get(\Szurubooru\Services\UpgradeService::class);
$upgradeService->runUpgradesVerbose();