diff --git a/TODO b/TODO index 608aefe7..0b81f952 100644 --- a/TODO +++ b/TODO @@ -121,9 +121,6 @@ refactors: - (idea) keep denormalized data in separate tables, i.e. tag usages in tag_meta or post favorite count in post_meta, so that it's obvious what can be recalculated on demand and what is real data - - change AbstractSearchService to AbstractSearchParser - - embed AbstractSearchService into Dao (it sucks to include XSearchService - together with Dao) - simplify template loading in presenters - right now template loading requires _, util and promise: promise.wait(util.loadTemplate('blah')) @@ -131,6 +128,12 @@ refactors: template = _.template(html); }); - change content spinner to nprogress: http://ricostacruz.com/nprogress/ + - add fetchUsers to PostSearchFilter: when AbstractDao fetches entities + with query decorated by PostDao::decorateQueryFromFilter, call + PostDao::decorateEntitiesWithFilter that will optimally load users and + inject them using posts' lazy loaders. + - make view proxies less greedy, e.g. favs should be fetched only in post + view, not for each of 40 posts in post list. (same goes to other things) miscellaneous: - fix mouse trap hotkeys when leaving page diff --git a/public_html/js/Presenters/LoginPresenter.js b/public_html/js/Presenters/LoginPresenter.js index 5081075f..9dd558b9 100644 --- a/public_html/js/Presenters/LoginPresenter.js +++ b/public_html/js/Presenters/LoginPresenter.js @@ -45,7 +45,7 @@ App.Presenters.LoginPresenter = function( var userNameOrEmail = $el.find('[name=user]').val(); var password = $el.find('[name=password]').val(); - var remember = $el.find('[name=remember]').val(); + var remember = $el.find('[name=remember]').is(':checked'); if (userNameOrEmail.length === 0) { messagePresenter.showError($messages, 'User name cannot be empty.'); diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php index 2b2b5dc8..7dd5ec75 100644 --- a/src/Controllers/PostController.php +++ b/src/Controllers/PostController.php @@ -38,11 +38,11 @@ final class PostController extends AbstractController { $formData = new \Szurubooru\FormData\SearchFormData($this->inputReader); $searchResult = $this->postService->getFiltered($formData); - $entities = $this->postViewProxy->fromArray($searchResult->entities); + $entities = $this->postViewProxy->fromArray($searchResult->getEntities()); return [ 'data' => $entities, - 'pageSize' => $searchResult->filter->pageSize, - 'totalRecords' => $searchResult->totalRecords]; + 'pageSize' => $searchResult->getPageSize(), + 'totalRecords' => $searchResult->getTotalRecords()]; } public function createPost() diff --git a/src/Controllers/UserController.php b/src/Controllers/UserController.php index e3e9ff38..b65515c3 100644 --- a/src/Controllers/UserController.php +++ b/src/Controllers/UserController.php @@ -48,11 +48,11 @@ final class UserController extends AbstractController $formData = new \Szurubooru\FormData\SearchFormData($this->inputReader); $searchResult = $this->userService->getFiltered($formData); - $entities = $this->userViewProxy->fromArray($searchResult->entities); + $entities = $this->userViewProxy->fromArray($searchResult->getEntities()); return [ 'data' => $entities, - 'pageSize' => $searchResult->filter->pageSize, - 'totalRecords' => $searchResult->totalRecords]; + 'pageSize' => $searchResult->getPageSize(), + 'totalRecords' => $searchResult->getTotalRecords()]; } public function createUser() diff --git a/src/Dao/AbstractDao.php b/src/Dao/AbstractDao.php index 45f06d66..4bf96a09 100644 --- a/src/Dao/AbstractDao.php +++ b/src/Dao/AbstractDao.php @@ -54,14 +54,9 @@ abstract class AbstractDao implements ICrudDao public function findAll() { - $entities = []; $query = $this->fpdo->from($this->tableName); - foreach ($query as $arrayEntity) - { - $entity = $this->entityConverter->toEntity($arrayEntity); - $entities[$entity->getId()] = $entity; - } - return $entities; + $arrayEntities = iterator_to_array($query); + return $this->arrayToEntities($arrayEntities); } public function findById($entityId) @@ -74,6 +69,46 @@ abstract class AbstractDao implements ICrudDao return $this->findBy($this->getIdColumn(), $entityIds); } + public function findFiltered(\Szurubooru\SearchServices\AbstractSearchFilter $searchFilter) + { + $orderByString = $this->compileOrderBy($searchFilter->order); + + $query = $this->fpdo + ->from($this->tableName) + ->orderBy($orderByString); + + $this->decorateQueryFromFilter($query, $searchFilter); + + return $this->arrayToEntities(iterator_to_array($query)); + } + + public function findFilteredAndPaged(\Szurubooru\SearchServices\AbstractSearchFilter $searchFilter, $pageNumber, $pageSize) + { + $orderByString = $this->compileOrderBy($searchFilter->order); + + $query = $this->fpdo + ->from($this->tableName) + ->orderBy($orderByString) + ->limit($pageSize) + ->offset($pageSize * ($pageNumber - 1)); + + $this->decorateQueryFromFilter($query, $searchFilter); + + $entities = $this->arrayToEntities(iterator_to_array($query)); + $query = $this->fpdo + ->from($this->tableName) + ->select('COUNT(1) AS c'); + $totalRecords = intval(iterator_to_array($query)[0]['c']); + + $pagedSearchResult = new \Szurubooru\SearchServices\PagedSearchResult(); + $pagedSearchResult->setSearchFilter($searchFilter); + $pagedSearchResult->setEntities($entities); + $pagedSearchResult->setTotalRecords($totalRecords); + $pagedSearchResult->setPageNumber($pageNumber); + $pagedSearchResult->setPageSize($pageSize); + return $pagedSearchResult; + } + public function deleteAll() { foreach ($this->findAll() as $entity) @@ -153,4 +188,27 @@ abstract class AbstractDao implements ICrudDao protected function beforeDelete(\Szurubooru\Entities\Entity $entity) { } + + protected function decorateQueryFromFilter($query, \Szurubooru\SearchServices\AbstractSearchFilter $filter) + { + } + + protected function arrayToEntities(array $arrayEntities) + { + $entities = []; + foreach ($arrayEntities as $arrayEntity) + { + $entity = $this->entityConverter->toEntity($arrayEntity); + $entities[$entity->getId()] = $entity; + } + return $entities; + } + + private function compileOrderBy($order) + { + $orderByString = ''; + foreach ($order as $orderColumn => $orderDir) + $orderByString .= $orderColumn . ' ' . ($orderDir === \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC ? 'DESC' : 'ASC') . ', '; + return substr($orderByString, 0, -2); + } } diff --git a/src/Dao/SearchResult.php b/src/Dao/SearchResult.php index 24e1582a..490e2362 100644 --- a/src/Dao/SearchResult.php +++ b/src/Dao/SearchResult.php @@ -14,4 +14,3 @@ class SearchResult $this->totalRecords = $totalRecords; } } - diff --git a/src/Dao/Services/AbstractSearchService.php b/src/Dao/Services/AbstractSearchService.php deleted file mode 100644 index 8aeb87ea..00000000 --- a/src/Dao/Services/AbstractSearchService.php +++ /dev/null @@ -1,134 +0,0 @@ -tableName = $dao->getTableName(); - $this->entityConverter = $dao->getEntityConverter(); - $this->fpdo = new \FluentPDO($databaseConnection->getPDO()); - } - - public function getFiltered( - \Szurubooru\Dao\SearchFilter $searchFilter) - { - list ($basicTokens, $complexTokens) = $this->tokenize($searchFilter->query); - - $filter = []; - if ($basicTokens) - $this->decorateFilterWithBasicTokens($filter, $basicTokens); - if ($complexTokens) - $this->decorateFilterWithComplexTokens($filter, $complexTokens); - - $order = $this->getOrder($searchFilter->order); - $pageSize = min(100, max(1, $searchFilter->pageSize)); - $pageNumber = max(1, $searchFilter->pageNumber) - 1; - - //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 ($query as $arrayEntity) - { - $entity = $this->entityConverter->toEntity($arrayEntity); - $entities[] = $entity; - } - - $query = $this->fpdo - ->from($this->tableName) - ->select('COUNT(1) AS c'); - $totalRecords = intval(iterator_to_array($query)[0]['c']); - return new \Szurubooru\Dao\SearchResult($searchFilter, $entities, $totalRecords); - } - - protected function decorateFilterWithBasicTokens($filter, $basicTokens) - { - throw new \BadMethodCallException('Not supported'); - } - - protected function decorateFilterWithComplexTokens($filter, $complexTokens) - { - throw new \BadMethodCallException('Not supported'); - } - - protected function getOrderColumn($token) - { - throw new \BadMethodCallException('Not supported'); - } - - protected function getDefaultOrderColumn() - { - return 'id'; - } - - protected function getDefaultOrderDir() - { - return self::ORDER_DESC; - } - - private function getOrder($query) - { - $order = []; - $tokens = array_filter(preg_split('/\s+/', $query)); - foreach ($tokens as $token) - { - $token = preg_split('/,|\s+/', $token); - $orderToken = $token[0]; - $orderDir = (count($token) === 2 and $token[1] === 'desc') ? self::ORDER_DESC : self::ORDER_ASC; - - $orderColumn = $this->getOrderColumn($orderToken); - if ($orderColumn === null) - throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken); - - $order[$orderColumn] = $orderDir; - } - $defaultOrderColumn = $this->getDefaultOrderColumn(); - $defaultOrderDir = $this->getDefaultOrderDir(); - if ($defaultOrderColumn) - $order[$defaultOrderColumn] = $defaultOrderDir; - return $order; - } - - private function tokenize($query) - { - $basicTokens = []; - $complexTokens = []; - - $tokens = array_filter(preg_split('/\s+/', $query)); - foreach ($tokens as $token) - { - if (strpos($token, ':') !== false) - { - list ($key, $value) = explode(':', $token, 1); - $complexTokens[$key] = $value; - } - else - { - $basicTokens[] = $token; - } - } - - return [$basicTokens, $complexTokens]; - } -} diff --git a/src/Dao/Services/PostSearchService.php b/src/Dao/Services/PostSearchService.php deleted file mode 100644 index 1594e54a..00000000 --- a/src/Dao/Services/PostSearchService.php +++ /dev/null @@ -1,12 +0,0 @@ -order = []; + } +} diff --git a/src/SearchServices/NamedSearchToken.php b/src/SearchServices/NamedSearchToken.php new file mode 100644 index 00000000..087bbede --- /dev/null +++ b/src/SearchServices/NamedSearchToken.php @@ -0,0 +1,7 @@ +searchFilter = $searchFilter; + } + + public function getSearchFilter() + { + return $this->searchFilter; + } + + public function setPageNumber($pageNumber) + { + $this->pageNumber = $pageNumber; + } + + public function getPageNumber() + { + return $this->pageNumber; + } + + public function setPageSize($pageSize) + { + $this->pageSize = $pageSize; + } + + public function getPageSize() + { + return $this->pageSize; + } + + public function setEntities(array $entities) + { + $this->entities = $entities; + } + + public function getEntities() + { + return $this->entities; + } + + public function setTotalRecords($totalRecords) + { + $this->totalRecords = $totalRecords; + } + + public function getTotalRecords() + { + return $this->totalRecords; + } +} diff --git a/src/SearchServices/Parsers/AbstractSearchParser.php b/src/SearchServices/Parsers/AbstractSearchParser.php new file mode 100644 index 00000000..f8468ac5 --- /dev/null +++ b/src/SearchServices/Parsers/AbstractSearchParser.php @@ -0,0 +1,103 @@ +createFilter(); + $filter->order = $this->getOrder($formData->order); + + $tokens = $this->tokenize($formData->query); + + foreach ($tokens as $token) + { + if ($token instanceof \Szurubooru\SearchServices\NamedSearchToken) + $this->decorateFilterFromNamedToken($filter, $token); + elseif ($token instanceof \Szurubooru\SearchService\SearchToken) + $this->decorateFilterFromToken($filter, $token); + else + throw new \RuntimeException('Invalid search token type'); + } + + return $filter; + } + + protected abstract function createFilter(); + + protected abstract function decorateFilterFromToken($filter, $token); + + protected abstract function decorateFilterFromNamedToken($filter, $namedToken); + + protected abstract function getOrderColumn($token); + + protected function getDefaultOrderColumn() + { + return 'id'; + } + + protected function getDefaultOrderDir() + { + return \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC; + } + + private function getOrder($query) + { + $order = []; + $tokens = array_filter(preg_split('/\s+/', $query)); + + foreach ($tokens as $token) + { + $token = preg_split('/,|\s+/', $token); + $orderToken = $token[0]; + $orderDir = (count($token) === 2 and $token[1] === 'desc') + ? \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_DESC + : \Szurubooru\SearchServices\AbstractSearchFilter::ORDER_ASC; + + $orderColumn = $this->getOrderColumn($orderToken); + if ($orderColumn === null) + throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken); + + $order[$orderColumn] = $orderDir; + } + + $defaultOrderColumn = $this->getDefaultOrderColumn(); + $defaultOrderDir = $this->getDefaultOrderDir(); + if ($defaultOrderColumn) + $order[$defaultOrderColumn] = $defaultOrderDir; + + return $order; + } + + private function tokenize($query) + { + $searchTokens = []; + + foreach (array_filter(preg_split('/\s+/', $query)) as $tokenText) + { + $negated = false; + if (substr($tokenText, 0, 1) === '-') + { + $negated = true; + $tokenText = substr($tokenText, 1); + } + + if (strpos($tokenText, ':') !== false) + { + $searchToken = new \Szurubooru\SearchServices\NamedSearchToken(); + list ($searchToken->key, $searchToken->value) = explode(':', $tokenText, 1); + + } + else + { + $searchToken = new \Szurubooru\SearchServices\SearchToken(); + $searchToken->value = $tokenText; + } + + $searchToken->negated = $negated; + $searchTokens[] = $searchToken; + } + + return $searchTokens; + } +} diff --git a/src/SearchServices/Parsers/PostSearchParser.php b/src/SearchServices/Parsers/PostSearchParser.php new file mode 100644 index 00000000..bae243cb --- /dev/null +++ b/src/SearchServices/Parsers/PostSearchParser.php @@ -0,0 +1,25 @@ +validator = $validator; $this->transactionManager = $transactionManager; $this->postDao = $postDao; - $this->postSearchService = $postSearchService; + $this->postSearchParser = $postSearchParser; $this->timeService = $timeService; $this->authService = $authService; $this->fileService = $fileService; @@ -66,8 +66,8 @@ class PostService $transactionFunc = function() use ($formData) { $this->validator->validate($formData); - $searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->posts->postsPerPage, $formData); - return $this->postSearchService->getFiltered($searchFilter); + $searchFilter = $this->postSearchParser->createFilterFromFormData($formData); + return $this->postDao->findFilteredAndPaged($searchFilter, $formData->pageNumber, $this->config->posts->postsPerPage); }; return $this->transactionManager->rollback($transactionFunc); } diff --git a/src/Services/UserService.php b/src/Services/UserService.php index baaa98ba..49b61822 100644 --- a/src/Services/UserService.php +++ b/src/Services/UserService.php @@ -7,7 +7,7 @@ class UserService private $validator; private $transactionManager; private $userDao; - private $userSearchService; + private $userSearchParser; private $passwordService; private $emailService; private $fileService; @@ -20,7 +20,7 @@ class UserService \Szurubooru\Validator $validator, \Szurubooru\Dao\TransactionManager $transactionManager, \Szurubooru\Dao\UserDao $userDao, - \Szurubooru\Dao\Services\UserSearchService $userSearchService, + \Szurubooru\SearchServices\Parsers\UserSearchParser $userSearchParser, \Szurubooru\Services\PasswordService $passwordService, \Szurubooru\Services\EmailService $emailService, \Szurubooru\Services\FileService $fileService, @@ -32,7 +32,7 @@ class UserService $this->validator = $validator; $this->transactionManager = $transactionManager; $this->userDao = $userDao; - $this->userSearchService = $userSearchService; + $this->userSearchParser = $userSearchParser; $this->passwordService = $passwordService; $this->emailService = $emailService; $this->fileService = $fileService; @@ -87,8 +87,8 @@ class UserService $transactionFunc = function() use ($formData) { $this->validator->validate($formData); - $searchFilter = new \Szurubooru\Dao\SearchFilter($this->config->users->usersPerPage, $formData); - return $this->userSearchService->getFiltered($searchFilter); + $searchFilter = $this->userSearchParser->createFilterFromFormData($formData); + return $this->userDao->findFilteredAndPaged($searchFilter, $formData->pageNumber, $this->config->users->usersPerPage); }; return $this->transactionManager->rollback($transactionFunc); } diff --git a/tests/Services/PostServiceTest.php b/tests/Services/PostServiceTest.php index 7d2da90b..b6923075 100644 --- a/tests/Services/PostServiceTest.php +++ b/tests/Services/PostServiceTest.php @@ -7,7 +7,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase private $validatorMock; private $transactionManagerMock; private $postDaoMock; - private $postSearchServiceMock; + private $postSearchParserMock; private $authServiceMock; private $timeServiceMock; private $fileServiceMock; @@ -19,7 +19,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->validatorMock = $this->mock(\Szurubooru\Validator::class); $this->transactionManagerMock = $this->mockTransactionManager(); $this->postDaoMock = $this->mock(\Szurubooru\Dao\PostDao::class); - $this->postSearchServiceMock = $this->mock(\Szurubooru\Dao\Services\PostSearchService::class); + $this->postSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\PostSearchParser::class); $this->authServiceMock = $this->mock(\Szurubooru\Services\AuthService::class); $this->timeServiceMock = $this->mock(\Szurubooru\Services\TimeService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); @@ -178,7 +178,7 @@ class PostServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->validatorMock, $this->transactionManagerMock, $this->postDaoMock, - $this->postSearchServiceMock, + $this->postSearchParserMock, $this->authServiceMock, $this->timeServiceMock, $this->fileServiceMock, diff --git a/tests/Services/UserServiceTest.php b/tests/Services/UserServiceTest.php index c347596a..00f072ae 100644 --- a/tests/Services/UserServiceTest.php +++ b/tests/Services/UserServiceTest.php @@ -7,7 +7,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase private $validatorMock; private $transactionManagerMock; private $userDaoMock; - private $userSearchServiceMock; + private $userSearchParserMock; private $passwordServiceMock; private $emailServiceMock; private $fileServiceMock; @@ -22,7 +22,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase $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); + $this->userSearchParserMock = $this->mock(\Szurubooru\SearchServices\Parsers\UserSearchParser::class); $this->passwordServiceMock = $this->mock(\Szurubooru\Services\PasswordService::class); $this->emailServiceMock = $this->mock(\Szurubooru\Services\EmailService::class); $this->fileServiceMock = $this->mock(\Szurubooru\Services\FileService::class); @@ -72,7 +72,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase $mockUser = new \Szurubooru\Entities\User; $mockUser->setName('user'); $expected = [$mockUser]; - $this->userSearchService->method('getFiltered')->willReturn($expected); + $this->userDaoMock->method('getFiltered')->willReturn($expected); $this->configMock->set('users/usersPerPage', 1); $searchFormData = new \Szurubooru\FormData\SearchFormData; @@ -292,7 +292,7 @@ final class UserServiceTest extends \Szurubooru\Tests\AbstractTestCase $this->validatorMock, $this->transactionManagerMock, $this->userDaoMock, - $this->userSearchService, + $this->userSearchParserMock, $this->passwordServiceMock, $this->emailServiceMock, $this->fileServiceMock,