From 5305bb68a4767f2c6e4b18e5b1c97d39dbd3e3b5 Mon Sep 17 00:00:00 2001 From: rr- Date: Wed, 25 Nov 2015 01:03:40 +0100 Subject: [PATCH] Simplified search parsing Reduced execution flow dependencies and made all search parsers share the basic code rather than implementing everything all over again in each parser through awkward protected functions. --- src/Routes/GetHistory.php | 11 +- src/Routes/Posts/GetPosts.php | 11 +- src/Routes/Tags/GetTags.php | 11 +- src/Routes/Users/GetUser.php | 9 +- src/Routes/Users/GetUsers.php | 11 +- .../AbstractSearchParserConfig.php | 266 +++++++++++++++ .../ParserConfigs/PostSearchParserConfig.php | 182 +++++++++++ .../SnapshotSearchParserConfig.php | 11 + .../ParserConfigs/TagSearchParserConfig.php | 63 ++++ .../ParserConfigs/UserSearchParserConfig.php | 22 ++ src/Search/Parsers/AbstractSearchParser.php | 275 ---------------- src/Search/Parsers/PostSearchParser.php | 309 ------------------ src/Search/Parsers/SnapshotSearchParser.php | 48 --- src/Search/Parsers/TagSearchParser.php | 77 ----- src/Search/Parsers/UserSearchParser.php | 34 -- src/Search/SearchParser.php | 137 ++++++++ 16 files changed, 710 insertions(+), 767 deletions(-) create mode 100644 src/Search/ParserConfigs/AbstractSearchParserConfig.php create mode 100644 src/Search/ParserConfigs/PostSearchParserConfig.php create mode 100644 src/Search/ParserConfigs/SnapshotSearchParserConfig.php create mode 100644 src/Search/ParserConfigs/TagSearchParserConfig.php create mode 100644 src/Search/ParserConfigs/UserSearchParserConfig.php delete mode 100644 src/Search/Parsers/AbstractSearchParser.php delete mode 100644 src/Search/Parsers/PostSearchParser.php delete mode 100644 src/Search/Parsers/SnapshotSearchParser.php delete mode 100644 src/Search/Parsers/TagSearchParser.php delete mode 100644 src/Search/Parsers/UserSearchParser.php create mode 100644 src/Search/SearchParser.php diff --git a/src/Routes/GetHistory.php b/src/Routes/GetHistory.php index 407c1e90..8f325ed2 100644 --- a/src/Routes/GetHistory.php +++ b/src/Routes/GetHistory.php @@ -3,7 +3,8 @@ namespace Szurubooru\Routes; use Szurubooru\Helpers\InputReader; use Szurubooru\Privilege; use Szurubooru\Routes\AbstractRoute; -use Szurubooru\Search\Parsers\SnapshotSearchParser; +use Szurubooru\Search\ParserConfigs\SnapshotSearchParserConfig; +use Szurubooru\Search\SearchParser; use Szurubooru\Services\HistoryService; use Szurubooru\Services\PrivilegeService; use Szurubooru\ViewProxies\SnapshotViewProxy; @@ -12,20 +13,20 @@ class GetHistory extends AbstractRoute { private $historyService; private $privilegeService; - private $snapshotSearchParser; + private $searchParser; private $inputReader; private $snapshotViewProxy; public function __construct( HistoryService $historyService, PrivilegeService $privilegeService, - SnapshotSearchParser $snapshotSearchParser, + SnapshotSearchParserConfig $searchParserConfig, InputReader $inputReader, SnapshotViewProxy $snapshotViewProxy) { $this->historyService = $historyService; $this->privilegeService = $privilegeService; - $this->snapshotSearchParser = $snapshotSearchParser; + $this->searchParser = new SearchParser($searchParserConfig); $this->inputReader = $inputReader; $this->snapshotViewProxy = $snapshotViewProxy; } @@ -44,7 +45,7 @@ class GetHistory extends AbstractRoute { $this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY); - $filter = $this->snapshotSearchParser->createFilterFromInputReader($this->inputReader); + $filter = $this->searchParser->createFilterFromInputReader($this->inputReader); $filter->setPageSize(50); $result = $this->historyService->getFiltered($filter); $entities = $this->snapshotViewProxy->fromArray($result->getEntities()); diff --git a/src/Routes/Posts/GetPosts.php b/src/Routes/Posts/GetPosts.php index f4b3bb84..8a4661f0 100644 --- a/src/Routes/Posts/GetPosts.php +++ b/src/Routes/Posts/GetPosts.php @@ -3,7 +3,8 @@ namespace Szurubooru\Routes\Posts; use Szurubooru\Config; use Szurubooru\Helpers\InputReader; use Szurubooru\Privilege; -use Szurubooru\Search\Parsers\PostSearchParser; +use Szurubooru\Search\SearchParser; +use Szurubooru\Search\ParserConfigs\PostSearchParserConfig; use Szurubooru\Services\PostService; use Szurubooru\Services\PrivilegeService; use Szurubooru\ViewProxies\PostViewProxy; @@ -13,7 +14,7 @@ class GetPosts extends AbstractPostRoute private $config; private $privilegeService; private $postService; - private $postSearchParser; + private $searchParser; private $inputReader; private $postViewProxy; @@ -21,14 +22,14 @@ class GetPosts extends AbstractPostRoute Config $config, PrivilegeService $privilegeService, PostService $postService, - PostSearchParser $postSearchParser, + PostSearchParserConfig $searchParserConfig, InputReader $inputReader, PostViewProxy $postViewProxy) { $this->config = $config; $this->privilegeService = $privilegeService; $this->postService = $postService; - $this->postSearchParser = $postSearchParser; + $this->searchParser = new SearchParser($searchParserConfig); $this->inputReader = $inputReader; $this->postViewProxy = $postViewProxy; } @@ -47,7 +48,7 @@ class GetPosts extends AbstractPostRoute { $this->privilegeService->assertPrivilege(Privilege::LIST_POSTS); - $filter = $this->postSearchParser->createFilterFromInputReader($this->inputReader); + $filter = $this->searchParser->createFilterFromInputReader($this->inputReader); $filter->setPageSize($this->config->posts->postsPerPage); $this->postService->decorateFilterFromBrowsingSettings($filter); diff --git a/src/Routes/Tags/GetTags.php b/src/Routes/Tags/GetTags.php index 30d4a09e..0e4469a7 100644 --- a/src/Routes/Tags/GetTags.php +++ b/src/Routes/Tags/GetTags.php @@ -2,7 +2,8 @@ namespace Szurubooru\Routes\Tags; use Szurubooru\Helpers\InputReader; use Szurubooru\Privilege; -use Szurubooru\Search\Parsers\TagSearchParser; +use Szurubooru\Search\ParserConfigs\TagSearchParserConfig; +use Szurubooru\Search\SearchParser; use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\TagService; use Szurubooru\ViewProxies\TagViewProxy; @@ -12,20 +13,20 @@ class GetTags extends AbstractTagRoute private $privilegeService; private $tagService; private $tagViewProxy; - private $tagSearchParser; + private $searchParserConfig; private $inputReader; public function __construct( PrivilegeService $privilegeService, TagService $tagService, TagViewProxy $tagViewProxy, - TagSearchParser $tagSearchParser, + TagSearchParserConfig $searchParserConfig, InputReader $inputReader) { $this->privilegeService = $privilegeService; $this->tagService = $tagService; $this->tagViewProxy = $tagViewProxy; - $this->tagSearchParser = $tagSearchParser; + $this->searchParser = new SearchParser($searchParserConfig); $this->inputReader = $inputReader; } @@ -43,7 +44,7 @@ class GetTags extends AbstractTagRoute { $this->privilegeService->assertPrivilege(Privilege::LIST_TAGS); - $filter = $this->tagSearchParser->createFilterFromInputReader($this->inputReader); + $filter = $this->searchParser->createFilterFromInputReader($this->inputReader); $filter->setPageSize(50); $result = $this->tagService->getFiltered($filter); diff --git a/src/Routes/Users/GetUser.php b/src/Routes/Users/GetUser.php index 89d0f0ea..7926619b 100644 --- a/src/Routes/Users/GetUser.php +++ b/src/Routes/Users/GetUser.php @@ -1,7 +1,8 @@ privilegeService = $privilegeService; $this->userService = $userService; - $this->userSearchParser = $userSearchParser; + $this->searchParser = new SearchParser($searchParserConfig); $this->userViewProxy = $userViewProxy; } diff --git a/src/Routes/Users/GetUsers.php b/src/Routes/Users/GetUsers.php index 81bb3e63..3a481d89 100644 --- a/src/Routes/Users/GetUsers.php +++ b/src/Routes/Users/GetUsers.php @@ -3,7 +3,8 @@ namespace Szurubooru\Routes\Users; use Szurubooru\Config; use Szurubooru\Helpers\InputReader; use Szurubooru\Privilege; -use Szurubooru\Search\Parsers\UserSearchParser; +use Szurubooru\Search\ParserConfigs\UserSearchParserConfig; +use Szurubooru\Search\SearchParser; use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\UserService; use Szurubooru\ViewProxies\UserViewProxy; @@ -13,7 +14,7 @@ class GetUsers extends AbstractUserRoute private $config; private $privilegeService; private $userService; - private $userSearchParser; + private $searchParser; private $inputReader; private $userViewProxy; @@ -21,14 +22,14 @@ class GetUsers extends AbstractUserRoute Config $config, PrivilegeService $privilegeService, UserService $userService, - UserSearchParser $userSearchParser, + UserSearchParserConfig $searchParserConfig, InputReader $inputReader, UserViewProxy $userViewProxy) { $this->config = $config; $this->privilegeService = $privilegeService; $this->userService = $userService; - $this->userSearchParser = $userSearchParser; + $this->searchParser = new SearchParser($searchParserConfig); $this->inputReader = $inputReader; $this->userViewProxy = $userViewProxy; } @@ -47,7 +48,7 @@ class GetUsers extends AbstractUserRoute { $this->privilegeService->assertPrivilege(Privilege::LIST_USERS); - $filter = $this->userSearchParser->createFilterFromInputReader($this->inputReader); + $filter = $this->searchParser->createFilterFromInputReader($this->inputReader); $filter->setPageSize($this->config->users->usersPerPage); $result = $this->userService->getFiltered($filter); $entities = $this->userViewProxy->fromArray($result->getEntities()); diff --git a/src/Search/ParserConfigs/AbstractSearchParserConfig.php b/src/Search/ParserConfigs/AbstractSearchParserConfig.php new file mode 100644 index 00000000..0874cfa2 --- /dev/null +++ b/src/Search/ParserConfigs/AbstractSearchParserConfig.php @@ -0,0 +1,266 @@ +orderAliasMap; + + foreach ($map as $item) + { + list ($aliases, $value) = $item; + if (self::matches($tokenValue, $aliases)) + return $value; + } + + throw new NotSupportedException('Unknown order term: ' . $tokenValue + . '. Possible order terms: ' + . join(', ', array_map(function($term) { return join('/', $term[0]); }, $map))); + } + + public function getRequirementForBasicToken(SearchToken $token) + { + if ($this->basicTokenParser) + { + $tmp = $this->basicTokenParser; + return $tmp($token); + } + throw new NotSupportedException('Basic tokens are not valid in this search'); + } + + public function getRequirementForNamedToken(NamedSearchToken $token) + { + if (self::matches($token->getKey(), ['special'])) + { + foreach ($this->specialTokenParsers as $item) + { + if (!self::matches($token->getValue(), $item->aliases)) + continue; + + $tmp = $item->callback; + return $tmp($token); + } + + $this->raiseNamedTokenError($token->getValue(), $this->specialTokenParsers); + } + + if ((strpos($token->getKey(), 'min') !== false + || strpos($token->getKey(), 'max') !== false) + && strpos($token->getValue(), '..') === false) + { + foreach ($this->namedTokenParsers as $item) + { + if (is_callable($item->flagsOrCallback) || + !($item->flagsOrCallback & self::ALLOW_RANGE)) + { + continue; + } + + foreach ($item->aliases as $alias) + { + if (!self::matches($token->getKey(), [$alias . '_min', $alias . '_max'])) + continue; + + $pseudoToken = new NamedSearchToken(); + $pseudoToken->setKey($alias); + $pseudoToken->setValue(strpos($token->getKey(), 'min') !== false + ? $token->getValue() . '..' + : '..' . $token->getValue()); + return $this->getRequirementForNamedToken($pseudoToken); + } + } + } + + foreach ($this->namedTokenParsers as $item) + { + if (!self::matches($token->getKey(), $item->aliases)) + continue; + + if (is_callable($item->flagsOrCallback)) + { + $tmp = $item->flagsOrCallback; + $requirementValue = $tmp($token->getValue()); + } + else + { + $requirementValue = $this->createRequirementValue( + $token->getValue(), + $item->flagsOrCallback); + } + + $requirement = new Requirement(); + $requirement->setType($item->columnName); + $requirement->setValue($requirementValue); + $requirement->setNegated($token->isNegated()); + return $requirement; + } + + $this->raiseNamedTokenError($token->getKey(), $this->namedTokenParsers); + } + + protected function defineOrder($columnName, array $aliases) + { + $this->orderAliasMap []= [$aliases, $columnName]; + } + + protected function defineBasicTokenParser($parser) + { + $this->basicTokenParser = $parser; + } + + protected function defineNamedTokenParser( + $columnName, + array $aliases, + $flagsOrCallback = 0) + { + $item = new \StdClass; + $item->columnName = $columnName; + $item->aliases = $aliases; + $item->flagsOrCallback = $flagsOrCallback; + $this->namedTokenParsers []= $item; + } + + protected function defineSpecialTokenParser( + array $aliases, + $callback) + { + $item = new \StdClass; + $item->aliases = $aliases; + $item->callback = $callback; + $this->specialTokenParsers []= $item; + } + + protected static function createRequirementValue( + $text, $flags = 0) + { + if (($flags & self::ALLOW_RANGE) === self::ALLOW_RANGE + && substr_count($text, '..') === 1) + { + list ($minValue, $maxValue) = explode('..', $text); + $value = new RequirementRangedValue(); + $value->setMinValue($minValue); + $value->setMaxValue($maxValue); + return $value; + } + + if (($flags & self::ALLOW_COMPOSITE) === self::ALLOW_COMPOSITE + && strpos($text, ',') !== false) + { + $values = explode(',', $text); + $value = new RequirementCompositeValue(); + $value->setValues($values); + return $value; + } + + return new RequirementSingleValue($text); + } + + protected function createDateRequirementValue($value) + { + if (substr_count($value, '..') === 1) + { + list ($dateMin, $dateMax) = explode('..', $value); + $timeMin = self::convertDateTime($dateMin)[0]; + $timeMax = self::convertDateTime($dateMax)[1]; + } + else + { + $date = $value; + list ($timeMin, $timeMax) = self::convertDateTime($date); + } + + $value = new RequirementRangedValue(); + $value->setMinValue(date('c', $timeMin)); + $value->setMaxValue(date('c', $timeMax)); + return $value; + } + + protected static function matches($text, $array) + { + $text = self::transformText($text); + foreach ($array as $elem) + { + if (self::transformText($elem) === $text) + return true; + } + return false; + } + + private static function transformText($text) + { + return str_replace('_', '', strtolower($text)); + } + + private static function convertDateTime($value) + { + $value = strtolower(trim($value)); + if (!$value) + { + return null; + } + elseif ($value === 'today') + { + $timeMin = mktime(0, 0, 0); + $timeMax = mktime(24, 0, -1); + } + elseif ($value === 'yesterday') + { + $timeMin = mktime(-24, 0, 0); + $timeMax = mktime(0, 0, -1); + } + elseif (preg_match('/^(\d{4})$/', $value, $matches)) + { + $year = intval($matches[1]); + $timeMin = mktime(0, 0, 0, 1, 1, $year); + $timeMax = mktime(0, 0, -1, 1, 1, $year + 1); + } + elseif (preg_match('/^(\d{4})-(\d{1,2})$/', $value, $matches)) + { + $year = intval($matches[1]); + $month = intval($matches[2]); + $timeMin = mktime(0, 0, 0, $month, 1, $year); + $timeMax = mktime(0, 0, -1, $month + 1, 1, $year); + } + elseif (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value, $matches)) + { + $year = intval($matches[1]); + $month = intval($matches[2]); + $day = intval($matches[3]); + $timeMin = mktime(0, 0, 0, $month, $day, $year); + $timeMax = mktime(0, 0, -1, $month, $day + 1, $year); + } + else + throw new \Exception('Invalid date format: ' . $value); + + return [$timeMin, $timeMax]; + } + + private function raiseNamedTokenError($key, array $parsers) + { + if (empty($parsers)) + throw new NotSupportedException('Such search is not supported in this context.'); + + throw new NotSupportedException( + 'Unknown search key: ' . $key + . '. Possible search keys: ' + . join(', ', array_map(function($item) { return join('/', $item->aliases); }, $parsers))); + } +} diff --git a/src/Search/ParserConfigs/PostSearchParserConfig.php b/src/Search/ParserConfigs/PostSearchParserConfig.php new file mode 100644 index 00000000..df9ec80f --- /dev/null +++ b/src/Search/ParserConfigs/PostSearchParserConfig.php @@ -0,0 +1,182 @@ +authService = $authService; + $this->privilegeService = $privilegeService; + + $this->defineOrder(PostFilter::ORDER_ID, ['id']); + $this->defineOrder(PostFilter::ORDER_RANDOM, ['random']); + $this->defineOrder(PostFilter::ORDER_CREATION_TIME, ['creation_time', 'creation_date', 'date']); + $this->defineOrder(PostFilter::ORDER_LAST_EDIT_TIME, ['edit_time', 'edit_date']); + $this->defineOrder(PostFilter::ORDER_SCORE, ['score']); + $this->defineOrder(PostFilter::ORDER_FILE_SIZE, ['file_size']); + $this->defineOrder(PostFilter::ORDER_TAG_COUNT, ['tag_count', 'tags', 'tag']); + $this->defineOrder(PostFilter::ORDER_FAV_COUNT, ['fav_count', 'fags', 'fav']); + $this->defineOrder(PostFilter::ORDER_COMMENT_COUNT, ['comment_count', 'comments', 'comment']); + $this->defineOrder(PostFilter::ORDER_NOTE_COUNT, ['note_count', 'notes', 'note']); + $this->defineOrder(PostFilter::ORDER_LAST_FAV_TIME, ['fav_time', 'fav_date']); + $this->defineOrder(PostFilter::ORDER_LAST_COMMENT_TIME, ['comment_time', 'comment_date']); + $this->defineOrder(PostFilter::ORDER_LAST_FEATURE_TIME, ['feature_time', 'feature_date']); + $this->defineOrder(PostFilter::ORDER_FEATURE_COUNT, ['feature_count', 'features', 'featured']); + + $this->defineBasicTokenParser( + function(SearchToken $token) + { + $requirement = new Requirement(); + $requirement->setNegated($token->isNegated()); + $requirement->setType(PostFilter::REQUIREMENT_TAG); + $requirement->setValue(new RequirementSingleValue($token->getValue())); + return $requirement; + }); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_ID, + ['id'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_HASH, + ['hash', 'name'], + self::ALLOW_COMPOSITE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_CREATION_TIME, + ['creation_date', 'creation_time', 'date', 'time'], + function ($value) + { + return self::createDateRequirementValue($value); + }); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_LAST_EDIT_TIME, + ['edit_date', 'edit_time'], + function ($value) + { + return self::createDateRequirementValue($value); + }); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_TAG_COUNT, + ['tag_count', 'tags'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_FAV_COUNT, + ['fav_count', 'favs'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_COMMENT_COUNT, + ['comment_count', 'comments'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_NOTE_COUNT, + ['note_count', 'notes'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_SCORE, + ['score'], + self::ALLOW_COMPOSITE | self::ALLOW_RANGE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_UPLOADER, + ['uploader', 'uploader', 'uploaded', 'submit', 'submitter', 'submitted'], + self::ALLOW_COMPOSITE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_SAFETY, + ['safety', 'rating'], + function ($value) + { + return self::createRequirementValue( + EnumHelper::postSafetyFromString($value), + self::ALLOW_COMPOSITE); + }); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_FAVORITE, + ['fav'], + self::ALLOW_COMPOSITE); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_TYPE, + ['type'], + function ($value) + { + return new RequirementSingleValue( + EnumHelper::postTypeFromString($value), + self::ALLOW_COMPOSITE); + }); + + $this->defineNamedTokenParser( + PostFilter::REQUIREMENT_COMMENT_AUTHOR, + ['comment', 'comment_author', 'commented'], + self::ALLOW_COMPOSITE); + + $this->defineSpecialTokenParser( + ['liked'], + function (SearchToken $token) + { + return $this->createOwnScoreRequirement(1, $token->isNegated()); + }); + + $this->defineSpecialTokenParser( + ['disliked'], + function (SearchToken $token) + { + return $this->createOwnScoreRequirement(-1, $token->isNegated()); + }); + + $this->defineSpecialTokenParser( + ['fav'], + function (SearchToken $token) + { + $this->privilegeService->assertLoggedIn(); + $token = new NamedSearchToken(); + $token->setKey('fav'); + $token->setValue($this->authService->getLoggedInUser()->getName()); + return $this->getRequirementForNamedToken($token); + }); + } + + public function createFilter() + { + return new PostFilter; + } + + private function createOwnScoreRequirement($score, $isNegated) + { + $this->privilegeService->assertLoggedIn(); + $userName = $this->authService->getLoggedInUser()->getName(); + $tokenValue = new RequirementCompositeValue(); + $tokenValue->setValues([$userName, $score]); + $requirement = new Requirement(); + $requirement->setType(PostFilter::REQUIREMENT_USER_SCORE); + $requirement->setValue($tokenValue); + $requirement->setNegated($isNegated); + return $requirement; + } +} diff --git a/src/Search/ParserConfigs/SnapshotSearchParserConfig.php b/src/Search/ParserConfigs/SnapshotSearchParserConfig.php new file mode 100644 index 00000000..2b3e2c43 --- /dev/null +++ b/src/Search/ParserConfigs/SnapshotSearchParserConfig.php @@ -0,0 +1,11 @@ +defineOrder(TagFilter::ORDER_ID, ['id']); + $this->defineOrder(TagFilter::ORDER_NAME, ['name']); + $this->defineOrder(TagFilter::ORDER_CREATION_TIME, ['creation_time']); + $this->defineOrder(TagFilter::ORDER_LAST_EDIT_TIME, ['edit_time']); + $this->defineOrder(TagFilter::ORDER_USAGE_COUNT, ['usage_count']); + + $this->defineBasicTokenParser( + function(SearchToken $token) + { + $requirement = new Requirement(); + $requirement->setType(TagFilter::REQUIREMENT_PARTIAL_TAG_NAME); + $requirement->setValue(new RequirementSingleValue($token->getValue())); + $requirement->setNegated($token->isNegated()); + return $requirement; + }); + + $this->defineNamedTokenParser( + TagFilter::REQUIREMENT_CREATION_TIME, + ['creation_time', 'creation_date', 'date', 'time'], + function ($value) + { + return self::createDateRequirementValue($value); + }); + + $this->defineNamedTokenParser( + TagFilter::REQUIREMENT_LAST_EDIT_TIME, + ['edit_time', 'edit_date'], + function ($value) + { + return self::createDateRequirementValue($value); + }); + + $this->defineNamedTokenParser( + TagFilter::REQUIREMENT_USAGE_COUNT, + ['usage_count', 'usages', 'usage'], + self::ALLOW_RANGE | self::ALLOW_COMPOSITE); + + $this->defineNamedTokenParser( + TagFilter::REQUIREMENT_CATEGORY, + ['category'], + self::ALLOW_COMPOSITE); + } + + public function createFilter() + { + return new TagFilter; + } +} diff --git a/src/Search/ParserConfigs/UserSearchParserConfig.php b/src/Search/ParserConfigs/UserSearchParserConfig.php new file mode 100644 index 00000000..d7652b1c --- /dev/null +++ b/src/Search/ParserConfigs/UserSearchParserConfig.php @@ -0,0 +1,22 @@ +defineOrder(UserFilter::ORDER_NAME, ['name']); + $this->defineOrder(UserFilter::ORDER_CREATION_TIME, ['creation_time', 'creation_date']); + } + + public function createFilter() + { + return new UserFilter; + } +} diff --git a/src/Search/Parsers/AbstractSearchParser.php b/src/Search/Parsers/AbstractSearchParser.php deleted file mode 100644 index a2f8937b..00000000 --- a/src/Search/Parsers/AbstractSearchParser.php +++ /dev/null @@ -1,275 +0,0 @@ -createFilter(); - $filter->setOrder($this->getOrder($inputReader->order, false) + $filter->getOrder()); - - if ($inputReader->page) - { - $filter->setPageNumber($inputReader->page); - $filter->setPageSize(25); - } - - $tokens = $this->tokenize($inputReader->query); - - foreach ($tokens as $token) - { - if ($token instanceof NamedSearchToken) - { - if ($token->getKey() === 'order') - $filter->setOrder($this->getOrder($token->getValue(), $token->isNegated()) + $filter->getOrder()); - else - $this->decorateFilterFromNamedToken($filter, $token); - } - elseif ($token instanceof SearchToken) - $this->decorateFilterFromToken($filter, $token); - else - throw new \RuntimeException('Invalid search token type: ' . get_class($token)); - } - - return $filter; - } - - protected abstract function createFilter(); - - protected abstract function decorateFilterFromToken(IFilter $filter, SearchToken $token); - - protected abstract function decorateFilterFromNamedToken(IFilter $filter, NamedSearchToken $namedToken); - - protected abstract function getOrderColumnMap(); - - protected function createRequirementValue($text, $flags = 0, callable $valueDecorator = null) - { - if ($valueDecorator === null) - { - $valueDecorator = function($value) - { - return $value; - }; - } - - if ((($flags & self::ALLOW_RANGES) === self::ALLOW_RANGES) && substr_count($text, '..') === 1) - { - list ($minValue, $maxValue) = explode('..', $text); - $minValue = $valueDecorator($minValue); - $maxValue = $valueDecorator($maxValue); - $tokenValue = new RequirementRangedValue(); - $tokenValue->setMinValue($minValue); - $tokenValue->setMaxValue($maxValue); - return $tokenValue; - } - else if ((($flags & self::ALLOW_COMPOSITE) === self::ALLOW_COMPOSITE) && strpos($text, ',') !== false) - { - $values = explode(',', $text); - $values = array_map($valueDecorator, $values); - $tokenValue = new RequirementCompositeValue(); - $tokenValue->setValues($values); - return $tokenValue; - } - - $value = $valueDecorator($text); - return new RequirementSingleValue($value); - } - - protected function addRequirementFromToken($filter, $token, $type, $flags, callable $valueDecorator = null) - { - $requirement = new Requirement(); - $requirement->setType($type); - $requirement->setValue($this->createRequirementValue($token->getValue(), $flags, $valueDecorator)); - $requirement->setNegated($token->isNegated()); - $filter->addRequirement($requirement); - } - - protected function addRequirementFromDateRangeToken($filter, $token, $type) - { - if (substr_count($token->getValue(), '..') === 1) - { - list ($dateMin, $dateMax) = explode('..', $token->getValue()); - $timeMin = $this->dateToTime($dateMin)[0]; - $timeMax = $this->dateToTime($dateMax)[1]; - } - else - { - $date = $token->getValue(); - list ($timeMin, $timeMax) = $this->dateToTime($date); - } - - $finalString = ''; - if ($timeMin) - $finalString .= date('c', $timeMin); - $finalString .= '..'; - if ($timeMax) - $finalString .= date('c', $timeMax); - - $token->setValue($finalString); - $this->addRequirementFromToken($filter, $token, $type, self::ALLOW_RANGES); - } - - private function getOrderColumn($tokenText) - { - $map = $this->getOrderColumnMap(); - - foreach ($map as $item) - { - list ($aliases, $value) = $item; - if ($this->matches($tokenText, $aliases)) - return $value; - } - - throw new NotSupportedException('Unknown order term: ' . $tokenText - . '. Possible order terms: ' - . join(', ', array_map(function($term) { return join('/', $term[0]); }, $map))); - } - - private function getOrder($query, $negated) - { - $order = []; - $tokens = array_filter(preg_split('/\s+/', trim($query))); - - foreach ($tokens as $token) - { - $token = preg_split('/,|\s+/', $token); - $orderToken = $token[0]; - - if (count($token) === 1) - { - $orderDir = IFilter::ORDER_DESC; - } - elseif (count($token) === 2) - { - if ($token[1] === 'desc') - $orderDir = IFilter::ORDER_DESC; - elseif ($token[1] === 'asc') - $orderDir = IFilter::ORDER_ASC; - else - throw new \Exception('Wrong search order direction'); - } - else - throw new \Exception('Wrong search order token'); - - $orderColumn = $this->getOrderColumn($orderToken); - if ($orderColumn === null) - throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken); - - if ($negated) - { - $orderDir = $orderDir == IFilter::ORDER_DESC ? IFilter::ORDER_ASC : IFilter::ORDER_DESC; - } - - - $order[$orderColumn] = $orderDir; - } - - return $order; - } - - private function tokenize($query) - { - $searchTokens = []; - - foreach (array_filter(preg_split('/\s+/', trim($query))) as $tokenText) - { - $negated = false; - if (substr($tokenText, 0, 1) === '-') - { - $negated = true; - $tokenText = substr($tokenText, 1); - } - - $colonPosition = strpos($tokenText, ':'); - if (($colonPosition !== false) && ($colonPosition > 0)) - { - $searchToken = new NamedSearchToken(); - list ($tokenKey, $tokenValue) = explode(':', $tokenText, 2); - $searchToken->setKey($tokenKey); - $searchToken->setValue($tokenValue); - } - else - { - $searchToken = new SearchToken(); - $searchToken->setValue($tokenText); - } - - $searchToken->setNegated($negated); - $searchTokens[] = $searchToken; - } - - return $searchTokens; - } - - protected function matches($text, $array) - { - $text = $this->transformText($text); - foreach ($array as $elem) - { - if ($this->transformText($elem) === $text) - return true; - } - return false; - } - - protected function transformText($text) - { - return str_replace('_', '', strtolower($text)); - } - - private function dateToTime($value) - { - $value = strtolower(trim($value)); - if (!$value) - { - return null; - } - elseif ($value === 'today') - { - $timeMin = mktime(0, 0, 0); - $timeMax = mktime(24, 0, -1); - } - elseif ($value === 'yesterday') - { - $timeMin = mktime(-24, 0, 0); - $timeMax = mktime(0, 0, -1); - } - elseif (preg_match('/^(\d{4})$/', $value, $matches)) - { - $year = intval($matches[1]); - $timeMin = mktime(0, 0, 0, 1, 1, $year); - $timeMax = mktime(0, 0, -1, 1, 1, $year + 1); - } - elseif (preg_match('/^(\d{4})-(\d{1,2})$/', $value, $matches)) - { - $year = intval($matches[1]); - $month = intval($matches[2]); - $timeMin = mktime(0, 0, 0, $month, 1, $year); - $timeMax = mktime(0, 0, -1, $month + 1, 1, $year); - } - elseif (preg_match('/^(\d{4})-(\d{1,2})-(\d{1,2})$/', $value, $matches)) - { - $year = intval($matches[1]); - $month = intval($matches[2]); - $day = intval($matches[3]); - $timeMin = mktime(0, 0, 0, $month, $day, $year); - $timeMax = mktime(0, 0, -1, $month, $day + 1, $year); - } - else - throw new \Exception('Invalid date format: ' . $value); - - return [$timeMin, $timeMax]; - } -} diff --git a/src/Search/Parsers/PostSearchParser.php b/src/Search/Parsers/PostSearchParser.php deleted file mode 100644 index 0908c39b..00000000 --- a/src/Search/Parsers/PostSearchParser.php +++ /dev/null @@ -1,309 +0,0 @@ -authService = $authService; - $this->privilegeService = $privilegeService; - } - - protected function createFilter() - { - return new PostFilter; - } - - protected function decorateFilterFromToken(IFilter $filter, SearchToken $token) - { - $requirement = new Requirement(); - $requirement->setType(PostFilter::REQUIREMENT_TAG); - $requirement->setValue($this->createRequirementValue($token->getValue())); - $requirement->setNegated($token->isNegated()); - $filter->addRequirement($requirement); - } - - protected function decorateFilterFromNamedToken(IFilter $filter, NamedSearchToken $token) - { - $tokenKey = $token->getKey(); - $tokenValue = $token->getValue(); - - $countAliases = - [ - 'tag_count' => 'tags', - 'fav_count' => 'favs', - 'score' => 'score', - 'comment_count' => 'comments', - 'note_count' => 'notes', - ]; - foreach ($countAliases as $realKey => $baseAlias) - { - if ($this->matches($tokenKey, [$baseAlias . '_min', $baseAlias . '_max'])) - { - $token = new NamedSearchToken(); - $token->setKey($realKey); - $token->setValue(strpos($tokenKey, 'min') !== false ? $tokenValue . '..' : '..' . $tokenValue); - return $this->decorateFilterFromNamedToken($filter, $token); - } - } - - $map = - [ - [['id'], [$this, 'addIdRequirement']], - [['hash', 'name'], [$this, 'addHashRequirement']], - [['date', 'time', 'creation_date', 'creation_time'], [$this, 'addCreationTimeRequirement']], - [['edit_date', 'edit_time'], [$this, 'addLastEditTimeRequirement']], - [['tag_count', 'tags'], [$this, 'addTagCountRequirement']], - [['fav_count', 'favs'], [$this, 'addFavCountRequirement']], - [['comment_count', 'comments'], [$this, 'addCommentCountRequirement']], - [['note_count', 'notes'], [$this, 'addNoteCountRequirement']], - [['score'], [$this, 'addScoreRequirement']], - [['uploader', 'uploader', 'uploaded', 'submit', 'submitter', 'submitted'], [$this, 'addUploaderRequirement']], - [['safety', 'rating'], [$this, 'addSafetyRequirement']], - [['fav'], [$this, 'addFavRequirement']], - [['type'], [$this, 'addTypeRequirement']], - [['comment', 'comment_author', 'commented'], [$this, 'addCommentAuthorRequirement']], - ]; - - foreach ($map as $item) - { - list ($aliases, $callback) = $item; - if ($this->matches($tokenKey, $aliases)) - return $callback($filter, $token); - } - - if ($this->matches($tokenKey, ['special'])) - { - $specialMap = - [ - [['liked'], [$this, 'addOwnLikedRequirement']], - [['disliked'], [$this, 'addOwnDislikedRequirement']], - [['fav'], [$this, 'addOwnFavRequirement']], - ]; - - foreach ($specialMap as $item) - { - list ($aliases, $callback) = $item; - if ($this->matches($token->getValue(), $aliases)) - return $callback($filter, $token); - } - - throw new NotSupportedException( - 'Unknown value for special search term: ' . $token->getValue() - . '. Possible search terms: ' - . join(', ', array_map(function($term) { return join('/', $term[0]); }, $specialMap))); - } - - throw new NotSupportedException('Unknown search term: ' . $token->getKey() - . '. Possible search terms: special, ' - . join(', ', array_map(function($term) { return join('/', $term[0]); }, $map))); - } - - protected function getOrderColumnMap() - { - return - [ - [['id'], PostFilter::ORDER_ID], - [['random'], PostFilter::ORDER_RANDOM], - [['creation_time', 'creation_date', 'date'], PostFilter::ORDER_CREATION_TIME], - [['edit_time', 'edit_date'], PostFilter::ORDER_LAST_EDIT_TIME], - [['score'], PostFilter::ORDER_SCORE], - [['file_size'], PostFilter::ORDER_FILE_SIZE], - [['tag_count', 'tags', 'tag'], PostFilter::ORDER_TAG_COUNT], - [['fav_count', 'fags', 'fav'], PostFilter::ORDER_FAV_COUNT], - [['comment_count', 'comments', 'comment'], PostFilter::ORDER_COMMENT_COUNT], - [['note_count', 'notes', 'note'], PostFilter::ORDER_NOTE_COUNT], - [['fav_time', 'fav_date'], PostFilter::ORDER_LAST_FAV_TIME], - [['comment_time', 'comment_date'], PostFilter::ORDER_LAST_COMMENT_TIME], - [['feature_time', 'feature_date'], PostFilter::ORDER_LAST_FEATURE_TIME], - [['feature_count', 'features', 'featured'], PostFilter::ORDER_FEATURE_COUNT], - ]; - } - - private function addOwnLikedRequirement($filter, $token) - { - $this->privilegeService->assertLoggedIn(); - $this->addUserScoreRequirement( - $filter, - $this->authService->getLoggedInUser()->getName(), - 1, - $token->isNegated()); - } - - private function addOwnDislikedRequirement($filter, $token) - { - $this->privilegeService->assertLoggedIn(); - $this->addUserScoreRequirement( - $filter, - $this->authService->getLoggedInUser()->getName(), - -1, - $token->isNegated()); - } - - private function addOwnFavRequirement($filter, $token) - { - $this->privilegeService->assertLoggedIn(); - $token = new NamedSearchToken(); - $token->setKey('fav'); - $token->setValue($this->authService->getLoggedInUser()->getName()); - $this->decorateFilterFromNamedToken($filter, $token); - } - - private function addIdRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_ID, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addHashRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_HASH, - self::ALLOW_COMPOSITE); - } - - private function addCreationTimeRequirement($filter, $token) - { - $this->addRequirementFromDateRangeToken( - $filter, - $token, - PostFilter::REQUIREMENT_CREATION_TIME); - } - - private function addLastEditTimeRequirement($filter, $token) - { - $this->addRequirementFromDateRangeToken( - $filter, - $token, - PostFilter::REQUIREMENT_LAST_EDIT_TIME); - } - - private function addTagCountRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_TAG_COUNT, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addFavCountRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_FAV_COUNT, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addCommentCountRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_COMMENT_COUNT, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addNoteCountRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_NOTE_COUNT, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addScoreRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_SCORE, - self::ALLOW_COMPOSITE | self::ALLOW_RANGES); - } - - private function addUploaderRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_UPLOADER, - self::ALLOW_COMPOSITE); - } - - private function addSafetyRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_SAFETY, - self::ALLOW_COMPOSITE, - function ($value) - { - return EnumHelper::postSafetyFromString($value); - }); - } - - private function addFavRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_FAVORITE, - self::ALLOW_COMPOSITE); - } - - private function addTypeRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_TYPE, - self::ALLOW_COMPOSITE, - function ($value) - { - return EnumHelper::postTypeFromString($value); - }); - } - - private function addCommentAuthorRequirement($filter, $token) - { - $this->addRequirementFromToken( - $filter, - $token, - PostFilter::REQUIREMENT_COMMENT_AUTHOR, - self::ALLOW_COMPOSITE); - } - - private function addUserScoreRequirement($filter, $userName, $score, $isNegated) - { - $tokenValue = new RequirementCompositeValue(); - $tokenValue->setValues([$userName, $score]); - $requirement = new Requirement(); - $requirement->setType(PostFilter::REQUIREMENT_USER_SCORE); - $requirement->setValue($tokenValue); - $requirement->setNegated($isNegated); - $filter->addRequirement($requirement); - } -} diff --git a/src/Search/Parsers/SnapshotSearchParser.php b/src/Search/Parsers/SnapshotSearchParser.php deleted file mode 100644 index 18a78374..00000000 --- a/src/Search/Parsers/SnapshotSearchParser.php +++ /dev/null @@ -1,48 +0,0 @@ -getValue(), ',') !== 1) - throw new NotSupportedException('Expected token in form of "type,id"'); - - if ($token->isNegated()) - throw new NotSupportedException('Negative searches are not supported in this context'); - - list ($type, $primaryKey) = explode(',', $token->getValue()); - - $requirement = new Requirement(); - $requirement->setType(SnapshotFilter::REQUIREMENT_PRIMARY_KEY); - $requirement->setValue($this->createRequirementValue($primaryKey)); - $filter->addRequirement($requirement); - - $requirement = new Requirement(); - $requirement->setType(SnapshotFilter::REQUIREMENT_TYPE); - $requirement->setValue($this->createRequirementValue(EnumHelper::snapshotTypeFromString($type))); - $filter->addRequirement($requirement); - } - - protected function decorateFilterFromNamedToken(IFilter $filter, NamedSearchToken $namedToken) - { - throw new NotSupportedException('Named tokens are not supported in this context'); - } - - protected function getOrderColumnMap() - { - throw new NotSupportedException('Search order is not supported in this context'); - } -} diff --git a/src/Search/Parsers/TagSearchParser.php b/src/Search/Parsers/TagSearchParser.php deleted file mode 100644 index 7163ad60..00000000 --- a/src/Search/Parsers/TagSearchParser.php +++ /dev/null @@ -1,77 +0,0 @@ -setType(TagFilter::REQUIREMENT_PARTIAL_TAG_NAME); - $requirement->setValue(new RequirementSingleValue($token->getValue())); - $requirement->setNegated($token->isNegated()); - $filter->addRequirement($requirement); - } - - protected function decorateFilterFromNamedToken(IFilter $filter, NamedSearchToken $token) - { - if ($this->matches($token->getKey(), ['creation_time', 'creation_date', 'date'])) - { - $this->addRequirementFromDateRangeToken( - $filter, $token, TagFilter::REQUIREMENT_CREATION_TIME); - return; - } - - if ($this->matches($token->getKey(), ['edit_time', 'edit_date'])) - { - $this->addRequirementFromDateRangeToken( - $filter, $token, TagFilter::REQUIREMENT_LAST_EDIT_TIME); - return; - } - - if ($this->matches($token->getKey(), ['usage_count', 'usages', 'usage'])) - { - $this->addRequirementFromToken( - $filter, - $token, - TagFilter::REQUIREMENT_USAGE_COUNT, - self::ALLOW_RANGES | self::ALLOW_COMPOSITE); - return; - } - - if ($this->matches($token->getKey(), ['category'])) - { - $this->addRequirementFromToken( - $filter, - $token, - TagFilter::REQUIREMENT_CATEGORY, - self::ALLOW_COMPOSITE); - return; - } - - throw new NotSupportedException('Unknown token: ' . $token->getKey()); - } - - protected function getOrderColumnMap() - { - return - [ - [['id'], TagFilter::ORDER_ID], - [['name'], TagFilter::ORDER_NAME], - [['creation_time', 'creation_date', 'date'], TagFilter::ORDER_CREATION_TIME], - [['edit_time', 'edit_date'], TagFilter::ORDER_LAST_EDIT_TIME], - [['usage_count', 'usages'], TagFilter::ORDER_USAGE_COUNT], - ]; - } -} diff --git a/src/Search/Parsers/UserSearchParser.php b/src/Search/Parsers/UserSearchParser.php deleted file mode 100644 index 86aa0ac2..00000000 --- a/src/Search/Parsers/UserSearchParser.php +++ /dev/null @@ -1,34 +0,0 @@ -parserConfig = $parserConfig; + } + + public function createFilterFromInputReader(InputReader $inputReader) + { + $filter = $this->parserConfig->createFilter(); + $filter->setOrder($this->getOrder($inputReader->order, false) + $filter->getOrder()); + + if ($inputReader->page) + { + $filter->setPageNumber($inputReader->page); + $filter->setPageSize(25); + } + + $tokens = $this->tokenize($inputReader->query); + + foreach ($tokens as $token) + { + if ($token instanceof NamedSearchToken) + { + if ($token->getKey() === 'order') + { + $filter->setOrder( + $this->getOrder($token->getValue(), $token->isNegated()) + + $filter->getOrder()); + } + else + { + $requirement = $this->parserConfig->getRequirementForNamedToken($token); + $filter->addRequirement($requirement); + } + } + elseif ($token instanceof SearchToken) + { + $requirement = $this->parserConfig->getRequirementForBasicToken($token); + $filter->addRequirement($requirement); + } + else + { + throw new \RuntimeException('Invalid search token type: ' . get_class($token)); + } + } + + return $filter; + } + + private function getOrder($query, $negated) + { + $order = []; + $tokens = array_filter(preg_split('/\s+/', trim($query))); + + foreach ($tokens as $token) + { + $token = preg_split('/,|\s+/', $token); + $orderToken = $token[0]; + + if (count($token) === 1) + { + $orderDir = IFilter::ORDER_DESC; + } + elseif (count($token) === 2) + { + if ($token[1] === 'desc') + $orderDir = IFilter::ORDER_DESC; + elseif ($token[1] === 'asc') + $orderDir = IFilter::ORDER_ASC; + else + throw new \Exception('Wrong search order direction'); + } + else + throw new \Exception('Wrong search order token'); + + $orderColumn = $this->parserConfig->getColumnForTokenValue($orderToken); + if ($orderColumn === null) + throw new \InvalidArgumentException('Invalid search order token: ' . $orderToken); + + if ($negated) + { + $orderDir = $orderDir == IFilter::ORDER_DESC + ? IFilter::ORDER_ASC + : IFilter::ORDER_DESC; + } + + $order[$orderColumn] = $orderDir; + } + + return $order; + } + + private function tokenize($query) + { + $searchTokens = []; + + foreach (array_filter(preg_split('/\s+/', trim($query))) as $tokenText) + { + $negated = false; + if (substr($tokenText, 0, 1) === '-') + { + $negated = true; + $tokenText = substr($tokenText, 1); + } + + $colonPosition = strpos($tokenText, ':'); + if (($colonPosition !== false) && ($colonPosition > 0)) + { + $searchToken = new NamedSearchToken(); + list ($tokenKey, $tokenValue) = explode(':', $tokenText, 2); + $searchToken->setKey($tokenKey); + $searchToken->setValue($tokenValue); + } + else + { + $searchToken = new SearchToken(); + $searchToken->setValue($tokenText); + } + + $searchToken->setNegated($negated); + $searchTokens[] = $searchToken; + } + + return $searchTokens; + } +}