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
namespace Szurubooru;
final class DatabaseConnection
class DatabaseConnection
{
private $pdo;
private $config;

View file

@ -3,39 +3,55 @@ namespace Szurubooru\Services;
class TokenService
{
private $transactionManager;
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;
}
public function getByName($tokenName)
{
$token = $this->tokenDao->findByName($tokenName);
if (!$token)
throw new \InvalidArgumentException('Token with identifier "' . $tokenName . '" not found.');
return $token;
return $this->transactionManager->rollback(function() use ($tokenName)
{
$token = $this->tokenDao->findByName($tokenName);
if (!$token)
throw new \InvalidArgumentException('Token with identifier "' . $tokenName . '" not found.');
return $token;
});
}
public function invalidateByName($tokenName)
{
return $this->tokenDao->deleteByName($tokenName);
$this->transactionManager->commit(function() use ($tokenName)
{
$this->tokenDao->deleteByName($tokenName);
});
}
public function invalidateByAdditionalData($additionalData)
{
return $this->tokenDao->deleteByAdditionalData($additionalData);
$this->transactionManager->commit(function() use ($additionalData)
{
$this->tokenDao->deleteByAdditionalData($additionalData);
});
}
public function createAndSaveToken($additionalData, $tokenPurpose)
{
$token = new \Szurubooru\Entities\Token();
$token->setName(sha1(date('r') . uniqid() . microtime(true)));
$token->setAdditionalData($additionalData);
$token->setPurpose($tokenPurpose);
$this->invalidateByAdditionalData($additionalData);
$this->tokenDao->save($token);
return $token;
return $this->transactionManager->commit(function() use ($additionalData, $tokenPurpose)
{
$token = new \Szurubooru\Entities\Token();
$token->setName(sha1(date('r') . uniqid() . microtime(true)));
$token->setAdditionalData($additionalData);
$token->setPurpose($tokenPurpose);
$this->invalidateByAdditionalData($additionalData);
$this->tokenDao->save($token);
return $token;
});
}
}

View file

@ -5,6 +5,7 @@ class UserService
{
private $config;
private $validator;
private $transactionManager;
private $userDao;
private $userSearchService;
private $passwordService;
@ -17,6 +18,7 @@ class UserService
public function __construct(
\Szurubooru\Config $config,
\Szurubooru\Validator $validator,
\Szurubooru\Dao\TransactionManager $transactionManager,
\Szurubooru\Dao\UserDao $userDao,
\Szurubooru\Dao\Services\UserSearchService $userSearchService,
\Szurubooru\Services\PasswordService $passwordService,
@ -28,6 +30,7 @@ class UserService
{
$this->config = $config;
$this->validator = $validator;
$this->transactionManager = $transactionManager;
$this->userDao = $userDao;
$this->userSearchService = $userSearchService;
$this->passwordService = $passwordService;
@ -40,110 +43,198 @@ class UserService
public function getByNameOrEmail($userNameOrEmail, $allowUnconfirmed = false)
{
$user = $this->userDao->findByName($userNameOrEmail);
if ($user)
return $user;
return $this->transactionManager->rollback(function() use ($userNameOrEmail, $allowUnconfirmed)
{
$user = $this->userDao->findByName($userNameOrEmail);
if ($user)
return $user;
$user = $this->userDao->findByEmail($userNameOrEmail, $allowUnconfirmed);
if ($user)
return $user;
$user = $this->userDao->findByEmail($userNameOrEmail, $allowUnconfirmed);
if ($user)
return $user;
throw new \InvalidArgumentException('User "' . $userNameOrEmail . '" was not found.');
throw new \InvalidArgumentException('User "' . $userNameOrEmail . '" was not found.');
});
}
public function getByName($userName)
{
$user = $this->userDao->findByName($userName);
if (!$user)
throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
return $user;
return $this->transactionManager->rollback(function() use ($userName)
{
$user = $this->userDao->findByName($userName);
if (!$user)
throw new \InvalidArgumentException('User with name "' . $userName . '" was not found.');
return $user;
});
}
public function getById($userId)
{
$user = $this->userDao->findById($userId);
if (!$user)
throw new \InvalidArgumentException('User with id "' . $userId . '" was not found.');
return $user;
return $this->transactionManager->rollback(function() use ($userId)
{
$user = $this->userDao->findById($userId);
if (!$user)
throw new \InvalidArgumentException('User with id "' . $userId . '" was not found.');
return $user;
});
}
public function getFiltered(\Szurubooru\FormData\SearchFormData $formData)
{
$this->validator->validate($formData);
$searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData);
return $this->userSearchService->getFiltered($searchFilter);
return $this->transactionManager->rollback(function() use ($formData)
{
$this->validator->validate($formData);
$searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData);
return $this->userSearchService->getFiltered($searchFilter);
});
}
public function createUser(\Szurubooru\FormData\RegistrationFormData $formData)
{
$formData->validate($this->validator);
return $this->transactionManager->commit(function() use ($formData)
{
$formData->validate($this->validator);
$user = new \Szurubooru\Entities\User();
$user->setRegistrationTime($this->timeService->getCurrentTime());
$user->setLastLoginTime(null);
$user->setAccessRank($this->userDao->hasAnyUsers()
? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER
: \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR);
$user = new \Szurubooru\Entities\User();
$user->setRegistrationTime($this->timeService->getCurrentTime());
$user->setLastLoginTime(null);
$user->setAccessRank($this->userDao->hasAnyUsers()
? \Szurubooru\Entities\User::ACCESS_RANK_REGULAR_USER
: \Szurubooru\Entities\User::ACCESS_RANK_ADMINISTRATOR);
$this->updateUserName($user, $formData->userName);
$this->updateUserPassword($user, $formData->password);
$this->updateUserAvatarStyle($user, \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR);
$this->updateUserEmail($user, $formData->email);
return $this->userDao->save($user);
$this->updateUserName($user, $formData->userName);
$this->updateUserPassword($user, $formData->password);
$this->updateUserAvatarStyle($user, \Szurubooru\Entities\User::AVATAR_STYLE_GRAVATAR);
$this->updateUserEmail($user, $formData->email);
return $this->userDao->save($user);
});
}
public function updateUser(\Szurubooru\Entities\User $user, \Szurubooru\FormData\UserEditFormData $formData)
{
$this->validator->validate($formData);
return $this->transactionManager->commit(function() use ($user, $formData)
{
$this->validator->validate($formData);
if ($formData->avatarStyle !== null)
$this->updateUserAvatarStyle($user, $formData->avatarStyle);
if ($formData->avatarStyle !== null)
$this->updateUserAvatarStyle($user, $formData->avatarStyle);
if ($formData->avatarContent !== null)
$this->updateUserAvatarContent($user, $formData->avatarContent);
if ($formData->avatarContent !== null)
$this->updateUserAvatarContent($user, $formData->avatarContent);
if ($formData->userName !== null)
$this->updateUserName($user, $formData->userName);
if ($formData->userName !== null)
$this->updateUserName($user, $formData->userName);
if ($formData->password !== null)
$this->updateUserPassword($user, $formData->password);
if ($formData->password !== null)
$this->updateUserPassword($user, $formData->password);
if ($formData->email !== null)
$this->updateUserEmail($user, $formData->email);
if ($formData->email !== null)
$this->updateUserEmail($user, $formData->email);
if ($formData->accessRank !== null)
$this->updateUserAccessRank($user, $formData->accessRank);
if ($formData->accessRank !== null)
$this->updateUserAccessRank($user, $formData->accessRank);
if ($formData->browsingSettings !== null)
$this->updateUserBrowsingSettings($user, $formData->browsingSettings);
if ($formData->browsingSettings !== null)
$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);
}
public function updateUserAvatarContent(\Szurubooru\Entities\User $user, $newAvatarContentInBase64)
private function updateUserAvatarContent(\Szurubooru\Entities\User $user, $newAvatarContentInBase64)
{
$target = $this->getCustomAvatarSourcePath($user);
$this->fileService->saveFromBase64($newAvatarContentInBase64, $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);
$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));
}
public function updateUserEmail(\Szurubooru\Entities\User $user, $newEmail)
private function updateUserEmail(\Szurubooru\Entities\User $user, $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);
}
public function updateUserBrowsingSettings(\Szurubooru\Entities\User $user, $newBrowsingSettings)
private function updateUserBrowsingSettings(\Szurubooru\Entities\User $user, $newBrowsingSettings)
{
$user->setBrowsingSettings($newBrowsingSettings);
}
public function updateUserLastLoginTime(\Szurubooru\Entities\User $user)
{
$user->setLastLoginTime($this->timeService->getCurrentTime());
$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';
$this->transactionManager->commit(function() use ($user)
{
$user->setLastLoginTime($this->timeService->getCurrentTime());
$this->userDao->save($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);
}

View file

@ -13,6 +13,11 @@ abstract class AbstractTestCase extends \PHPUnit_Framework_TestCase
return new ConfigMock($path);
}
public function mockTransactionManager()
{
return new TransactionManagerMock($this->mock(\Szurubooru\DatabaseConnection::class));
}
public function createTestDirectory()
{
$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 $validatorMock;
private $transactionManagerMock;
private $userDaoMock;
private $userSearchServiceMock;
private $passwordServiceMock;
@ -18,6 +19,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase
{
parent::setUp();
$this->configMock = $this->mockConfig();
$this->transactionManagerMock = $this->mockTransactionManager();
$this->validatorMock = $this->mock(\Szurubooru\Validator::class);
$this->userDaoMock = $this->mock(\Szurubooru\Dao\UserDao::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(
$this->configMock,
$this->validatorMock,
$this->transactionManagerMock,
$this->userDaoMock,
$this->userSearchService,
$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();
}
}