diff --git a/composer.json b/composer.json index fd0cf204..a935cbe7 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,7 @@ { "require": { "mnapoli/php-di": "~4.0", - "jbrooksuk/phpcheckstyle": "dev-master" + "jbrooksuk/phpcheckstyle": "dev-master", + "lichtner/fluentpdo": "dev-master" } } diff --git a/data/.gitignore b/data/.gitignore index 302e8399..a64f8e06 100644 --- a/data/.gitignore +++ b/data/.gitignore @@ -1,2 +1,4 @@ +db.sqlite +executed_upgrades.txt local.ini thumbnails diff --git a/data/config.ini b/data/config.ini index ec89bd70..3cca980d 100644 --- a/data/config.ini +++ b/data/config.ini @@ -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 diff --git a/gruntfile.js b/gruntfile.js index de7205f6..d23be177 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -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'); diff --git a/src/Dao/AbstractDao.php b/src/Dao/AbstractDao.php index 62df4082..abc29e6b 100644 --- a/src/Dao/AbstractDao.php +++ b/src/Dao/AbstractDao.php @@ -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(); } } diff --git a/src/Dao/EntityConverter.php b/src/Dao/EntityConverter.php index a2d7a356..4afba539 100644 --- a/src/Dao/EntityConverter.php +++ b/src/Dao/EntityConverter.php @@ -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; } } diff --git a/src/Dao/Services/AbstractSearchService.php b/src/Dao/Services/AbstractSearchService.php index 4f0a8152..e03264fc 100644 --- a/src/Dao/Services/AbstractSearchService.php +++ b/src/Dao/Services/AbstractSearchService.php @@ -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() diff --git a/src/Dao/Services/UserSearchService.php b/src/Dao/Services/UserSearchService.php index 3e9b2ab1..d1889643 100644 --- a/src/Dao/Services/UserSearchService.php +++ b/src/Dao/Services/UserSearchService.php @@ -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) diff --git a/src/Dao/TokenDao.php b/src/Dao/TokenDao.php index 8573306b..16534d56 100644 --- a/src/Dao/TokenDao.php +++ b/src/Dao/TokenDao.php @@ -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); } } diff --git a/src/Dao/UserDao.php b/src/Dao/UserDao.php index 5cb26e73..9bef3ca1 100644 --- a/src/Dao/UserDao.php +++ b/src/Dao/UserDao.php @@ -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); } } diff --git a/src/DatabaseConnection.php b/src/DatabaseConnection.php index 7c5840c9..6f993717 100644 --- a/src/DatabaseConnection.php +++ b/src/DatabaseConnection.php @@ -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); } } diff --git a/src/Entities/Token.php b/src/Entities/Token.php index af6f960a..3074839d 100644 --- a/src/Entities/Token.php +++ b/src/Entities/Token.php @@ -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; diff --git a/src/Services/UpgradeService.php b/src/Services/UpgradeService.php new file mode 100644 index 00000000..b805e61d --- /dev/null +++ b/src/Services/UpgradeService.php @@ -0,0 +1,75 @@ +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'; + } +} diff --git a/src/UpgradeService.php b/src/UpgradeService.php deleted file mode 100644 index c01dfa3e..00000000 --- a/src/UpgradeService.php +++ /dev/null @@ -1,29 +0,0 @@ -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(); - } -} diff --git a/src/Upgrades/IUpgrade.php b/src/Upgrades/IUpgrade.php new file mode 100644 index 00000000..27213f2d --- /dev/null +++ b/src/Upgrades/IUpgrade.php @@ -0,0 +1,7 @@ +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 + );'); + } +} diff --git a/src/Upgrades/UpgradeRepository.php b/src/Upgrades/UpgradeRepository.php new file mode 100644 index 00000000..fcf9f9b5 --- /dev/null +++ b/src/Upgrades/UpgradeRepository.php @@ -0,0 +1,17 @@ +upgrades = $upgrades; + } + + public function getUpgrades() + { + return $this->upgrades; + } +} diff --git a/src/di.php b/src/di.php index ec472a82..5d552718 100644 --- a/src/di.php +++ b/src/di.php @@ -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 [ diff --git a/tests/AbstractDatabaseTestCase.php b/tests/AbstractDatabaseTestCase.php index 4da39ef0..5e75b2aa 100644 --- a/tests/AbstractDatabaseTestCase.php +++ b/tests/AbstractDatabaseTestCase.php @@ -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(); } } diff --git a/tests/AbstractTestCase.php b/tests/AbstractTestCase.php index e15f236b..8a6a61e7 100644 --- a/tests/AbstractTestCase.php +++ b/tests/AbstractTestCase.php @@ -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'); diff --git a/tests/ControllerRepositoryTest.php b/tests/ControllerRepositoryTest.php index debe8a1d..36d33b82 100644 --- a/tests/ControllerRepositoryTest.php +++ b/tests/ControllerRepositoryTest.php @@ -1,11 +1,6 @@ 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; } } diff --git a/tests/Dao/TokenDaoTest.php b/tests/Dao/TokenDaoTest.php index 3f2d8b84..9c4fade4 100644 --- a/tests/Dao/TokenDaoTest.php +++ b/tests/Dao/TokenDaoTest.php @@ -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; diff --git a/tests/Dao/UserDaoTest.php b/tests/Dao/UserDaoTest.php index 86a085f6..11906cb6 100644 --- a/tests/Dao/UserDaoTest.php +++ b/tests/Dao/UserDaoTest.php @@ -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; + } } diff --git a/upgrade.php b/upgrade.php new file mode 100644 index 00000000..765e10c6 --- /dev/null +++ b/upgrade.php @@ -0,0 +1,6 @@ +runUpgradesVerbose();