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.
This commit is contained in:
rr- 2015-11-25 01:03:40 +01:00
parent 5aa75a4150
commit 5305bb68a4
16 changed files with 710 additions and 767 deletions

View file

@ -3,7 +3,8 @@ namespace Szurubooru\Routes;
use Szurubooru\Helpers\InputReader; use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege; use Szurubooru\Privilege;
use Szurubooru\Routes\AbstractRoute; 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\HistoryService;
use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\PrivilegeService;
use Szurubooru\ViewProxies\SnapshotViewProxy; use Szurubooru\ViewProxies\SnapshotViewProxy;
@ -12,20 +13,20 @@ class GetHistory extends AbstractRoute
{ {
private $historyService; private $historyService;
private $privilegeService; private $privilegeService;
private $snapshotSearchParser; private $searchParser;
private $inputReader; private $inputReader;
private $snapshotViewProxy; private $snapshotViewProxy;
public function __construct( public function __construct(
HistoryService $historyService, HistoryService $historyService,
PrivilegeService $privilegeService, PrivilegeService $privilegeService,
SnapshotSearchParser $snapshotSearchParser, SnapshotSearchParserConfig $searchParserConfig,
InputReader $inputReader, InputReader $inputReader,
SnapshotViewProxy $snapshotViewProxy) SnapshotViewProxy $snapshotViewProxy)
{ {
$this->historyService = $historyService; $this->historyService = $historyService;
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->snapshotSearchParser = $snapshotSearchParser; $this->searchParser = new SearchParser($searchParserConfig);
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
$this->snapshotViewProxy = $snapshotViewProxy; $this->snapshotViewProxy = $snapshotViewProxy;
} }
@ -44,7 +45,7 @@ class GetHistory extends AbstractRoute
{ {
$this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY); $this->privilegeService->assertPrivilege(Privilege::VIEW_HISTORY);
$filter = $this->snapshotSearchParser->createFilterFromInputReader($this->inputReader); $filter = $this->searchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize(50); $filter->setPageSize(50);
$result = $this->historyService->getFiltered($filter); $result = $this->historyService->getFiltered($filter);
$entities = $this->snapshotViewProxy->fromArray($result->getEntities()); $entities = $this->snapshotViewProxy->fromArray($result->getEntities());

View file

@ -3,7 +3,8 @@ namespace Szurubooru\Routes\Posts;
use Szurubooru\Config; use Szurubooru\Config;
use Szurubooru\Helpers\InputReader; use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege; use Szurubooru\Privilege;
use Szurubooru\Search\Parsers\PostSearchParser; use Szurubooru\Search\SearchParser;
use Szurubooru\Search\ParserConfigs\PostSearchParserConfig;
use Szurubooru\Services\PostService; use Szurubooru\Services\PostService;
use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\PrivilegeService;
use Szurubooru\ViewProxies\PostViewProxy; use Szurubooru\ViewProxies\PostViewProxy;
@ -13,7 +14,7 @@ class GetPosts extends AbstractPostRoute
private $config; private $config;
private $privilegeService; private $privilegeService;
private $postService; private $postService;
private $postSearchParser; private $searchParser;
private $inputReader; private $inputReader;
private $postViewProxy; private $postViewProxy;
@ -21,14 +22,14 @@ class GetPosts extends AbstractPostRoute
Config $config, Config $config,
PrivilegeService $privilegeService, PrivilegeService $privilegeService,
PostService $postService, PostService $postService,
PostSearchParser $postSearchParser, PostSearchParserConfig $searchParserConfig,
InputReader $inputReader, InputReader $inputReader,
PostViewProxy $postViewProxy) PostViewProxy $postViewProxy)
{ {
$this->config = $config; $this->config = $config;
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->postService = $postService; $this->postService = $postService;
$this->postSearchParser = $postSearchParser; $this->searchParser = new SearchParser($searchParserConfig);
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
$this->postViewProxy = $postViewProxy; $this->postViewProxy = $postViewProxy;
} }
@ -47,7 +48,7 @@ class GetPosts extends AbstractPostRoute
{ {
$this->privilegeService->assertPrivilege(Privilege::LIST_POSTS); $this->privilegeService->assertPrivilege(Privilege::LIST_POSTS);
$filter = $this->postSearchParser->createFilterFromInputReader($this->inputReader); $filter = $this->searchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize($this->config->posts->postsPerPage); $filter->setPageSize($this->config->posts->postsPerPage);
$this->postService->decorateFilterFromBrowsingSettings($filter); $this->postService->decorateFilterFromBrowsingSettings($filter);

View file

@ -2,7 +2,8 @@
namespace Szurubooru\Routes\Tags; namespace Szurubooru\Routes\Tags;
use Szurubooru\Helpers\InputReader; use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege; use Szurubooru\Privilege;
use Szurubooru\Search\Parsers\TagSearchParser; use Szurubooru\Search\ParserConfigs\TagSearchParserConfig;
use Szurubooru\Search\SearchParser;
use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\TagService; use Szurubooru\Services\TagService;
use Szurubooru\ViewProxies\TagViewProxy; use Szurubooru\ViewProxies\TagViewProxy;
@ -12,20 +13,20 @@ class GetTags extends AbstractTagRoute
private $privilegeService; private $privilegeService;
private $tagService; private $tagService;
private $tagViewProxy; private $tagViewProxy;
private $tagSearchParser; private $searchParserConfig;
private $inputReader; private $inputReader;
public function __construct( public function __construct(
PrivilegeService $privilegeService, PrivilegeService $privilegeService,
TagService $tagService, TagService $tagService,
TagViewProxy $tagViewProxy, TagViewProxy $tagViewProxy,
TagSearchParser $tagSearchParser, TagSearchParserConfig $searchParserConfig,
InputReader $inputReader) InputReader $inputReader)
{ {
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->tagService = $tagService; $this->tagService = $tagService;
$this->tagViewProxy = $tagViewProxy; $this->tagViewProxy = $tagViewProxy;
$this->tagSearchParser = $tagSearchParser; $this->searchParser = new SearchParser($searchParserConfig);
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
} }
@ -43,7 +44,7 @@ class GetTags extends AbstractTagRoute
{ {
$this->privilegeService->assertPrivilege(Privilege::LIST_TAGS); $this->privilegeService->assertPrivilege(Privilege::LIST_TAGS);
$filter = $this->tagSearchParser->createFilterFromInputReader($this->inputReader); $filter = $this->searchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize(50); $filter->setPageSize(50);
$result = $this->tagService->getFiltered($filter); $result = $this->tagService->getFiltered($filter);

View file

@ -1,7 +1,8 @@
<?php <?php
namespace Szurubooru\Routes\Users; namespace Szurubooru\Routes\Users;
use Szurubooru\Privilege; use Szurubooru\Privilege;
use Szurubooru\Search\Parsers\UserSearchParser; use Szurubooru\Search\ParserConfigs\UserSearchParserConfig;
use Szurubooru\Search\SearchParser;
use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\UserService; use Szurubooru\Services\UserService;
use Szurubooru\ViewProxies\UserViewProxy; use Szurubooru\ViewProxies\UserViewProxy;
@ -10,18 +11,18 @@ class GetUser extends AbstractUserRoute
{ {
private $privilegeService; private $privilegeService;
private $userService; private $userService;
private $userSearchParser; private $searchParserConfig;
private $userViewProxy; private $userViewProxy;
public function __construct( public function __construct(
PrivilegeService $privilegeService, PrivilegeService $privilegeService,
UserService $userService, UserService $userService,
UserSearchParser $userSearchParser, UserSearchParserConfig $searchParserConfig,
UserViewProxy $userViewProxy) UserViewProxy $userViewProxy)
{ {
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->userService = $userService; $this->userService = $userService;
$this->userSearchParser = $userSearchParser; $this->searchParser = new SearchParser($searchParserConfig);
$this->userViewProxy = $userViewProxy; $this->userViewProxy = $userViewProxy;
} }

View file

@ -3,7 +3,8 @@ namespace Szurubooru\Routes\Users;
use Szurubooru\Config; use Szurubooru\Config;
use Szurubooru\Helpers\InputReader; use Szurubooru\Helpers\InputReader;
use Szurubooru\Privilege; use Szurubooru\Privilege;
use Szurubooru\Search\Parsers\UserSearchParser; use Szurubooru\Search\ParserConfigs\UserSearchParserConfig;
use Szurubooru\Search\SearchParser;
use Szurubooru\Services\PrivilegeService; use Szurubooru\Services\PrivilegeService;
use Szurubooru\Services\UserService; use Szurubooru\Services\UserService;
use Szurubooru\ViewProxies\UserViewProxy; use Szurubooru\ViewProxies\UserViewProxy;
@ -13,7 +14,7 @@ class GetUsers extends AbstractUserRoute
private $config; private $config;
private $privilegeService; private $privilegeService;
private $userService; private $userService;
private $userSearchParser; private $searchParser;
private $inputReader; private $inputReader;
private $userViewProxy; private $userViewProxy;
@ -21,14 +22,14 @@ class GetUsers extends AbstractUserRoute
Config $config, Config $config,
PrivilegeService $privilegeService, PrivilegeService $privilegeService,
UserService $userService, UserService $userService,
UserSearchParser $userSearchParser, UserSearchParserConfig $searchParserConfig,
InputReader $inputReader, InputReader $inputReader,
UserViewProxy $userViewProxy) UserViewProxy $userViewProxy)
{ {
$this->config = $config; $this->config = $config;
$this->privilegeService = $privilegeService; $this->privilegeService = $privilegeService;
$this->userService = $userService; $this->userService = $userService;
$this->userSearchParser = $userSearchParser; $this->searchParser = new SearchParser($searchParserConfig);
$this->inputReader = $inputReader; $this->inputReader = $inputReader;
$this->userViewProxy = $userViewProxy; $this->userViewProxy = $userViewProxy;
} }
@ -47,7 +48,7 @@ class GetUsers extends AbstractUserRoute
{ {
$this->privilegeService->assertPrivilege(Privilege::LIST_USERS); $this->privilegeService->assertPrivilege(Privilege::LIST_USERS);
$filter = $this->userSearchParser->createFilterFromInputReader($this->inputReader); $filter = $this->searchParser->createFilterFromInputReader($this->inputReader);
$filter->setPageSize($this->config->users->usersPerPage); $filter->setPageSize($this->config->users->usersPerPage);
$result = $this->userService->getFiltered($filter); $result = $this->userService->getFiltered($filter);
$entities = $this->userViewProxy->fromArray($result->getEntities()); $entities = $this->userViewProxy->fromArray($result->getEntities());

View file

@ -0,0 +1,266 @@
<?php
namespace Szurubooru\Search\ParserConfigs;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementCompositeValue;
use Szurubooru\Search\Requirements\RequirementRangedValue;
use Szurubooru\Search\Requirements\RequirementSingleValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
abstract class AbstractSearchParserConfig
{
const ALLOW_COMPOSITE = 1;
const ALLOW_RANGE = 2;
private $orderAliasMap = [];
private $basicTokenParser = null;
private $namedTokenParsers = [];
private $specialTokenParsers = [];
public abstract function createFilter();
public function getColumnForTokenValue($tokenValue)
{
$map = $this->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)));
}
}

View file

@ -0,0 +1,182 @@
<?php
namespace Szurubooru\Search\ParserConfigs;
use Szurubooru\Helpers\EnumHelper;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\PostFilter;
use Szurubooru\Search\ParserConfigs\AbstractSearchParserConfig;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementSingleValue;
use Szurubooru\Search\Requirements\RequirementCompositeValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\PrivilegeService;
class PostSearchParserConfig extends AbstractSearchParserConfig
{
private $authService;
private $privilegeService;
public function __construct(
AuthService $authService,
PrivilegeService $privilegeService)
{
$this->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;
}
}

View file

@ -0,0 +1,11 @@
<?php
namespace Szurubooru\Search\ParserConfigs;
use Szurubooru\Search\Filters\SnapshotFilter;
class SnapshotSearchParserConfig extends AbstractSearchParserConfig
{
public function createFilter()
{
return new SnapshotFilter;
}
}

View file

@ -0,0 +1,63 @@
<?php
namespace Szurubooru\Search\ParserConfigs;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\TagFilter;
use Szurubooru\Search\ParserConfigs\AbstractSearchParserConfig;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementSingleValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class TagSearchParserConfig extends AbstractSearchParserConfig
{
public function __construct()
{
$this->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;
}
}

View file

@ -0,0 +1,22 @@
<?php
namespace Szurubooru\Search\ParserConfigs;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\UserFilter;
use Szurubooru\Search\ParserConfigs\AbstractSearchParserConfig;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class UserSearchParserConfig extends AbstractSearchParserConfig
{
public function __construct()
{
$this->defineOrder(UserFilter::ORDER_NAME, ['name']);
$this->defineOrder(UserFilter::ORDER_CREATION_TIME, ['creation_time', 'creation_date']);
}
public function createFilter()
{
return new UserFilter;
}
}

View file

@ -1,275 +0,0 @@
<?php
namespace Szurubooru\Search\Parsers;
use Szurubooru\Helpers\InputReader;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementCompositeValue;
use Szurubooru\Search\Requirements\RequirementRangedValue;
use Szurubooru\Search\Requirements\RequirementSingleValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
abstract class AbstractSearchParser
{
const ALLOW_COMPOSITE = 1;
const ALLOW_RANGES = 2;
public function createFilterFromInputReader(InputReader $inputReader)
{
$filter = $this->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];
}
}

View file

@ -1,309 +0,0 @@
<?php
namespace Szurubooru\Search\Parsers;
use Szurubooru\Helpers\EnumHelper;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\PostFilter;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementCompositeValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
use Szurubooru\Services\AuthService;
use Szurubooru\Services\PrivilegeService;
class PostSearchParser extends AbstractSearchParser
{
private $authService;
private $privilegeService;
public function __construct(
AuthService $authService,
PrivilegeService $privilegeService)
{
$this->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);
}
}

View file

@ -1,48 +0,0 @@
<?php
namespace Szurubooru\Search\Parsers;
use Szurubooru\Helpers\EnumHelper;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\SnapshotFilter;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class SnapshotSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new SnapshotFilter;
}
protected function decorateFilterFromToken(IFilter $filter, SearchToken $token)
{
if (substr_count($token->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');
}
}

View file

@ -1,77 +0,0 @@
<?php
namespace Szurubooru\Search\Parsers;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\TagFilter;
use Szurubooru\Search\Requirements\Requirement;
use Szurubooru\Search\Requirements\RequirementSingleValue;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class TagSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new TagFilter;
}
protected function decorateFilterFromToken(IFilter $filter, SearchToken $token)
{
$requirement = new Requirement();
$requirement->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],
];
}
}

View file

@ -1,34 +0,0 @@
<?php
namespace Szurubooru\Search\Parsers;
use Szurubooru\NotSupportedException;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\Filters\UserFilter;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class UserSearchParser extends AbstractSearchParser
{
protected function createFilter()
{
return new UserFilter;
}
protected function decorateFilterFromToken(IFilter $filter, SearchToken $token)
{
throw new NotSupportedException();
}
protected function decorateFilterFromNamedToken(IFilter $filter, NamedSearchToken $namedToken)
{
throw new NotSupportedException();
}
protected function getOrderColumnMap()
{
return
[
[['name'], UserFilter::ORDER_NAME],
[['creation_time', 'creation_date'], UserFilter::ORDER_CREATION_TIME],
];
}
}

137
src/Search/SearchParser.php Normal file
View file

@ -0,0 +1,137 @@
<?php
namespace Szurubooru\Search;
use Szurubooru\Helpers\InputReader;
use Szurubooru\Search\Filters\IFilter;
use Szurubooru\Search\ParserConfigs\AbstractSearchParserConfig;
use Szurubooru\Search\Tokens\NamedSearchToken;
use Szurubooru\Search\Tokens\SearchToken;
class SearchParser
{
private $parserConfig;
public function __construct(AbstractSearchParserConfig $parserConfig)
{
$this->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;
}
}