Added transaction manager

This commit is contained in:
Marcin Kurczewski 2014-09-14 18:41:14 +02:00
parent f71fd106f0
commit 6035cf89b7
8 changed files with 324 additions and 126 deletions

View file

@ -0,0 +1,42 @@
<?php
namespace Szurubooru\Dao;
class TransactionManager
{
private $databaseConnection;
public function __construct(\Szurubooru\DatabaseConnection $databaseConnection)
{
$this->databaseConnection = $databaseConnection;
}
public function commit($callback)
{
return $this->doInTransaction($callback, 'commit');
}
public function rollback($callback)
{
return $this->doInTransaction($callback, 'rollBack');
}
public function doInTransaction($callback, $operation)
{
$pdo = $this->databaseConnection->getPDO();
if ($pdo->inTransaction())
return $callback();
try
{
$pdo->beginTransaction();
$ret = $callback();
$pdo->$operation();
return $ret;
}
catch (\Exception $e)
{
$pdo->rollBack();
throw $e;
}
}
}

View file

@ -1,7 +1,7 @@
<?php <?php
namespace Szurubooru; namespace Szurubooru;
final class DatabaseConnection class DatabaseConnection
{ {
private $pdo; private $pdo;
private $config; private $config;

View file

@ -3,32 +3,47 @@ namespace Szurubooru\Services;
class TokenService class TokenService
{ {
private $transactionManager;
private $tokenDao; private $tokenDao;
public function __construct(\Szurubooru\Dao\TokenDao $tokenDao) public function __construct(
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\TokenDao $tokenDao)
{ {
$this->transactionManager = $transactionManager;
$this->tokenDao = $tokenDao; $this->tokenDao = $tokenDao;
} }
public function getByName($tokenName) public function getByName($tokenName)
{
return $this->transactionManager->rollback(function() use ($tokenName)
{ {
$token = $this->tokenDao->findByName($tokenName); $token = $this->tokenDao->findByName($tokenName);
if (!$token) if (!$token)
throw new \InvalidArgumentException('Token with identifier "' . $tokenName . '" not found.'); throw new \InvalidArgumentException('Token with identifier "' . $tokenName . '" not found.');
return $token; return $token;
});
} }
public function invalidateByName($tokenName) public function invalidateByName($tokenName)
{ {
return $this->tokenDao->deleteByName($tokenName); $this->transactionManager->commit(function() use ($tokenName)
{
$this->tokenDao->deleteByName($tokenName);
});
} }
public function invalidateByAdditionalData($additionalData) public function invalidateByAdditionalData($additionalData)
{ {
return $this->tokenDao->deleteByAdditionalData($additionalData); $this->transactionManager->commit(function() use ($additionalData)
{
$this->tokenDao->deleteByAdditionalData($additionalData);
});
} }
public function createAndSaveToken($additionalData, $tokenPurpose) public function createAndSaveToken($additionalData, $tokenPurpose)
{
return $this->transactionManager->commit(function() use ($additionalData, $tokenPurpose)
{ {
$token = new \Szurubooru\Entities\Token(); $token = new \Szurubooru\Entities\Token();
$token->setName(sha1(date('r') . uniqid() . microtime(true))); $token->setName(sha1(date('r') . uniqid() . microtime(true)));
@ -37,5 +52,6 @@ class TokenService
$this->invalidateByAdditionalData($additionalData); $this->invalidateByAdditionalData($additionalData);
$this->tokenDao->save($token); $this->tokenDao->save($token);
return $token; return $token;
});
} }
} }

View file

@ -5,6 +5,7 @@ class UserService
{ {
private $config; private $config;
private $validator; private $validator;
private $transactionManager;
private $userDao; private $userDao;
private $userSearchService; private $userSearchService;
private $passwordService; private $passwordService;
@ -17,6 +18,7 @@ class UserService
public function __construct( public function __construct(
\Szurubooru\Config $config, \Szurubooru\Config $config,
\Szurubooru\Validator $validator, \Szurubooru\Validator $validator,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\UserDao $userDao, \Szurubooru\Dao\UserDao $userDao,
\Szurubooru\Dao\Services\UserSearchService $userSearchService, \Szurubooru\Dao\Services\UserSearchService $userSearchService,
\Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\PasswordService $passwordService,
@ -28,6 +30,7 @@ class UserService
{ {
$this->config = $config; $this->config = $config;
$this->validator = $validator; $this->validator = $validator;
$this->transactionManager = $transactionManager;
$this->userDao = $userDao; $this->userDao = $userDao;
$this->userSearchService = $userSearchService; $this->userSearchService = $userSearchService;
$this->passwordService = $passwordService; $this->passwordService = $passwordService;
@ -39,6 +42,8 @@ class UserService
} }
public function getByNameOrEmail($userNameOrEmail, $allowUnconfirmed = false) public function getByNameOrEmail($userNameOrEmail, $allowUnconfirmed = false)
{
return $this->transactionManager->rollback(function() use ($userNameOrEmail, $allowUnconfirmed)
{ {
$user = $this->userDao->findByName($userNameOrEmail); $user = $this->userDao->findByName($userNameOrEmail);
if ($user) if ($user)
@ -49,32 +54,44 @@ class UserService
return $user; return $user;
throw new \InvalidArgumentException('User "' . $userNameOrEmail . '" was not found.'); throw new \InvalidArgumentException('User "' . $userNameOrEmail . '" was not found.');
});
} }
public function getByName($userName) public function getByName($userName)
{
return $this->transactionManager->rollback(function() use ($userName)
{ {
$user = $this->userDao->findByName($userName); $user = $this->userDao->findByName($userName);
if (!$user) if (!$user)
throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.'); throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
return $user; return $user;
});
} }
public function getById($userId) public function getById($userId)
{
return $this->transactionManager->rollback(function() use ($userId)
{ {
$user = $this->userDao->findById($userId); $user = $this->userDao->findById($userId);
if (!$user) if (!$user)
throw new \InvalidArgumentException('User with id "' . $userId . '" was not found.'); throw new \InvalidArgumentException('User with id "' . $userId . '" was not found.');
return $user; return $user;
});
} }
public function getFiltered(\Szurubooru\FormData\SearchFormData $formData) public function getFiltered(\Szurubooru\FormData\SearchFormData $formData)
{
return $this->transactionManager->rollback(function() use ($formData)
{ {
$this->validator->validate($formData); $this->validator->validate($formData);
$searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData); $searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData);
return $this->userSearchService->getFiltered($searchFilter); return $this->userSearchService->getFiltered($searchFilter);
});
} }
public function createUser(\Szurubooru\FormData\RegistrationFormData $formData) public function createUser(\Szurubooru\FormData\RegistrationFormData $formData)
{
return $this->transactionManager->commit(function() use ($formData)
{ {
$formData->validate($this->validator); $formData->validate($this->validator);
@ -90,9 +107,12 @@ class UserService
$this->updateUserAvatarStyle($user, \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR); $this->updateUserAvatarStyle($user, \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR);
$this->updateUserEmail($user, $formData->email); $this->updateUserEmail($user, $formData->email);
return $this->userDao->save($user); return $this->userDao->save($user);
});
} }
public function updateUser(\Szurubooru\Entities\User $user, \Szurubooru\FormData\UserEditFormData $formData) public function updateUser(\Szurubooru\Entities\User $user, \Szurubooru\FormData\UserEditFormData $formData)
{
return $this->transactionManager->commit(function() use ($user, $formData)
{ {
$this->validator->validate($formData); $this->validator->validate($formData);
@ -118,32 +138,103 @@ class UserService
$this->updateUserBrowsingSettings($user, $formData->browsingSettings); $this->updateUserBrowsingSettings($user, $formData->browsingSettings);
return $this->userDao->save($user); return $this->userDao->save($user);
});
} }
public function updateUserAvatarStyle(\Szurubooru\Entities\User $user, $newAvatarStyle) public function deleteUser(\Szurubooru\Entities\User $user)
{
$this->transactionManager->commit(function() use ($user)
{
$this->userDao->deleteById($user->getId());
$avatarSource = $this->getCustomAvatarSourcePath($user);
$this->fileService->delete($avatarSource);
$this->thumbnailService->deleteUsedThumbnails($avatarSource);
});
}
public function sendPasswordResetEmail(\Szurubooru\Entities\User $user)
{
$this->transactionManager->commit(function() use ($user)
{
$token = $this->tokenService->createAndSaveToken($user->getName(), \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET);
$this->emailService->sendPasswordResetEmail($user, $token);
});
}
public function finishPasswordReset(\Szurubooru\Entities\Token $token)
{
return $this->transactionManager->commit(function() use ($token)
{
if ($token->getPurpose() !== \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET)
throw new \Exception('This token is not a password reset token.');
$user = $this->getByName($token->getAdditionalData());
$newPassword = $this->passwordService->getRandomPassword();
$user->setPasswordHash($this->passwordService->getHash($newPassword));
$this->userDao->save($user);
$this->tokenService->invalidateByName($token->getName());
return $newPassword;
});
}
public function sendActivationEmail(\Szurubooru\Entities\User $user)
{
$this->transactionManager->commit(function() use ($user)
{
$token = $this->tokenService->createAndSaveToken($user->getName(), \Szurubooru\Entities\Token::PURPOSE_ACTIVATE);
$this->emailService->sendActivationEmail($user, $token);
});
}
public function finishActivation(\Szurubooru\Entities\Token $token)
{
$this->transactionManager->commit(function() use ($token)
{
if ($token->getPurpose() !== \Szurubooru\Entities\Token::PURPOSE_ACTIVATE)
throw new \Exception('This token is not an activation token.');
$user = $this->getByName($token->getAdditionalData());
$user = $this->confirmUserEmail($user);
$this->userDao->save($user);
$this->tokenService->invalidateByName($token->getName());
});
}
public function getCustomAvatarSourcePath(\Szurubooru\Entities\User $user)
{
return 'avatars' . DIRECTORY_SEPARATOR . $user->getId();
}
public function getBlankAvatarSourcePath()
{
return 'avatars' . DIRECTORY_SEPARATOR . 'blank.png';
}
private function updateUserAvatarStyle(\Szurubooru\Entities\User $user, $newAvatarStyle)
{ {
$user->setAvatarStyle($newAvatarStyle); $user->setAvatarStyle($newAvatarStyle);
} }
public function updateUserAvatarContent(\Szurubooru\Entities\User $user, $newAvatarContentInBase64) private function updateUserAvatarContent(\Szurubooru\Entities\User $user, $newAvatarContentInBase64)
{ {
$target = $this->getCustomAvatarSourcePath($user); $target = $this->getCustomAvatarSourcePath($user);
$this->fileService->saveFromBase64($newAvatarContentInBase64, $target); $this->fileService->saveFromBase64($newAvatarContentInBase64, $target);
$this->thumbnailService->deleteUsedThumbnails($target); $this->thumbnailService->deleteUsedThumbnails($target);
} }
public function updateUserName(\Szurubooru\Entities\User $user, $newName) private function updateUserName(\Szurubooru\Entities\User $user, $newName)
{ {
$this->assertNoUserWithThisName($user, $newName); $this->assertNoUserWithThisName($user, $newName);
$user->setName($newName); $user->setName($newName);
} }
public function updateUserPassword(\Szurubooru\Entities\User $user, $newPassword) private function updateUserPassword(\Szurubooru\Entities\User $user, $newPassword)
{ {
$user->setPasswordHash($this->passwordService->getHash($newPassword)); $user->setPasswordHash($this->passwordService->getHash($newPassword));
} }
public function updateUserEmail(\Szurubooru\Entities\User $user, $newEmail) private function updateUserEmail(\Szurubooru\Entities\User $user, $newEmail)
{ {
if ($user->getEmail() === $newEmail) if ($user->getEmail() === $newEmail)
{ {
@ -157,80 +248,29 @@ class UserService
} }
} }
public function updateUserAccessRank(\Szurubooru\Entities\User $user, $newAccessRank) private function updateUserAccessRank(\Szurubooru\Entities\User $user, $newAccessRank)
{ {
$user->setAccessRank($newAccessRank); $user->setAccessRank($newAccessRank);
} }
public function updateUserBrowsingSettings(\Szurubooru\Entities\User $user, $newBrowsingSettings) private function updateUserBrowsingSettings(\Szurubooru\Entities\User $user, $newBrowsingSettings)
{ {
$user->setBrowsingSettings($newBrowsingSettings); $user->setBrowsingSettings($newBrowsingSettings);
} }
public function updateUserLastLoginTime(\Szurubooru\Entities\User $user) public function updateUserLastLoginTime(\Szurubooru\Entities\User $user)
{
$this->transactionManager->commit(function() use ($user)
{ {
$user->setLastLoginTime($this->timeService->getCurrentTime()); $user->setLastLoginTime($this->timeService->getCurrentTime());
$this->userDao->save($user); $this->userDao->save($user);
} });
public function deleteUser(\Szurubooru\Entities\User $user)
{
$this->userDao->deleteById($user->getId());
$avatarSource = $this->getCustomAvatarSourcePath($user);
$this->fileService->delete($avatarSource);
$this->thumbnailService->deleteUsedThumbnails($avatarSource);
}
public function sendPasswordResetEmail(\Szurubooru\Entities\User $user)
{
$token = $this->tokenService->createAndSaveToken($user->getName(), \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET);
$this->emailService->sendPasswordResetEmail($user, $token);
}
public function finishPasswordReset(\Szurubooru\Entities\Token $token)
{
if ($token->getPurpose() !== \Szurubooru\Entities\Token::PURPOSE_PASSWORD_RESET)
throw new \Exception('This token is not a password reset token.');
$user = $this->getByName($token->getAdditionalData());
$newPassword = $this->passwordService->getRandomPassword();
$user->setPasswordHash($this->passwordService->getHash($newPassword));
$this->userDao->save($user);
$this->tokenService->invalidateByName($token->getName());
return $newPassword;
}
public function sendActivationEmail(\Szurubooru\Entities\User $user)
{
$token = $this->tokenService->createAndSaveToken($user->getName(), \Szurubooru\Entities\Token::PURPOSE_ACTIVATE);
$this->emailService->sendActivationEmail($user, $token);
}
public function finishActivation(\Szurubooru\Entities\Token $token)
{
if ($token->getPurpose() !== \Szurubooru\Entities\Token::PURPOSE_ACTIVATE)
throw new \Exception('This token is not an activation token.');
$user = $this->getByName($token->getAdditionalData());
$user = $this->confirmUserEmail($user);
$this->userDao->save($user);
$this->tokenService->invalidateByName($token->getName());
}
public function getCustomAvatarSourcePath(\Szurubooru\Entities\User $user)
{
return 'avatars' . DIRECTORY_SEPARATOR . $user->getId();
}
public function getBlankAvatarSourcePath()
{
return 'avatars' . DIRECTORY_SEPARATOR . 'blank.png';
} }
private function sendActivationEmailIfNeeded(\Szurubooru\Entities\User $user) private function sendActivationEmailIfNeeded(\Szurubooru\Entities\User $user)
{ {
if ($user->getAccessRank() === \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR or !$this->config->security->needEmailActivationToRegister) if ($user->getAccessRank() === \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR
or !$this->config->security->needEmailActivationToRegister)
{ {
$user = $this->confirmUserEmail($user); $user = $this->confirmUserEmail($user);
} }

View file

@ -13,6 +13,11 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
return new ConfigMock($path); return new ConfigMock($path);
} }
public function mockTransactionManager()
{
return new TransactionManagerMock($this->mock(\Szurubooru\DatabaseConnection::class));
}
public function createTestDirectory() public function createTestDirectory()
{ {
$path = $this->getTestDirectoryPath(); $path = $this->getTestDirectoryPath();

View file

@ -0,0 +1,77 @@
<?php
namespace Szurubooru\Tests\Dao;
class TransactionManagerTest extends \Szurubooru\Tests\AbstractDatabaseTestCase
{
public function testCommit()
{
$testEntity = $this->getTestEntity();
$testDao = $this->getTestDao();
$transactionManager = $this->getTransactionManager();
$transactionManager->commit(function() use ($testDao, &$testEntity)
{
$testDao->save($testEntity);
$this->assertNotNull($testEntity->getId());
});
$this->assertNotNull($testEntity->getId());
$this->assertEquals($testEntity, $testDao->findById($testEntity->getId()));
}
public function testRollback()
{
$testEntity = $this->getTestEntity();
$testDao = $this->getTestDao();
$transactionManager = $this->getTransactionManager();
$transactionManager->rollback(function() use ($testDao, &$testEntity)
{
$testDao->save($testEntity);
$this->assertNotNull($testEntity->getId());
});
//ids that could be forged in transaction get left behind after rollback
$this->assertNotNull($testEntity->getId());
//but entities shouldn't be saved to database
$this->assertNull($testDao->findById($testEntity->getId()));
}
public function testNestedTransactions()
{
$testEntity = $this->getTestEntity();
$testDao = $this->getTestDao();
$transactionManager = $this->getTransactionManager();
$transactionManager->commit(function() use ($transactionManager, $testDao, &$testEntity)
{
$transactionManager->commit(function() use ($testDao, &$testEntity)
{
$testDao->save($testEntity);
$this->assertNotNull($testEntity->getId());
});
});
$this->assertNotNull($testEntity->getId());
$this->assertEquals($testEntity, $testDao->findById($testEntity->getId()));
}
private function getTestEntity()
{
$token = new \Szurubooru\Entities\Token();
$token->setName('yo');
$token->setPurpose(\Szurubooru\Entities\Token::PURPOSE_ACTIVATE);
return $token;
}
private function getTestDao()
{
return \Szurubooru\Injector::get(\Szurubooru\Dao\TokenDao::class);
}
private function getTransactionManager()
{
return \Szurubooru\Injector::get(\Szurubooru\Dao\TransactionManager::class);
}
}

View file

@ -5,6 +5,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
{ {
private $configMock; private $configMock;
private $validatorMock; private $validatorMock;
private $transactionManagerMock;
private $userDaoMock; private $userDaoMock;
private $userSearchServiceMock; private $userSearchServiceMock;
private $passwordServiceMock; private $passwordServiceMock;
@ -18,6 +19,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
{ {
parent::setUp(); parent::setUp();
$this->configMock = $this->mockConfig(); $this->configMock = $this->mockConfig();
$this->transactionManagerMock = $this->mockTransactionManager();
$this->validatorMock = $this->mock(\Szurubooru\Validator::class); $this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class); $this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::class);
$this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class); $this->userSearchService = $this->mock(\Szurubooru\Dao\Services\UserSearchService::class);
@ -288,6 +290,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
return new \Szurubooru\Services\UserService( return new \Szurubooru\Services\UserService(
$this->configMock, $this->configMock,
$this->validatorMock, $this->validatorMock,
$this->transactionManagerMock,
$this->userDaoMock, $this->userDaoMock,
$this->userSearchService, $this->userSearchService,
$this->passwordServiceMock, $this->passwordServiceMock,

View file

@ -0,0 +1,15 @@
<?php
namespace Szurubooru\Tests;
class TransactionManagerMock extends \Szurubooru\Dao\TransactionManager
{
public function rollback($callback)
{
return $callback();
}
public function commit($callback)
{
return $callback();
}
}