diff --git a/app/Providers/SearchServiceProvider.php b/app/Providers/SearchServiceProvider.php index 1dcc9b8acc..cecd94c00e 100644 --- a/app/Providers/SearchServiceProvider.php +++ b/app/Providers/SearchServiceProvider.php @@ -22,7 +22,7 @@ declare(strict_types=1); namespace FireflyIII\Providers; -use FireflyIII\Support\Search\Search; +use FireflyIII\Support\Search\BetterQuerySearch; use FireflyIII\Support\Search\SearchInterface; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider; @@ -48,8 +48,8 @@ class SearchServiceProvider extends ServiceProvider $this->app->bind( SearchInterface::class, function (Application $app) { - /** @var Search $search */ - $search = app(Search::class); + /** @var BetterQuerySearch $search */ + $search = app(BetterQuerySearch::class); if ($app->auth->check()) { $search->setUser(auth()->user()); } diff --git a/app/Support/Search/BetterQuerySearch.php b/app/Support/Search/BetterQuerySearch.php new file mode 100644 index 0000000000..64d59cea20 --- /dev/null +++ b/app/Support/Search/BetterQuerySearch.php @@ -0,0 +1,302 @@ +. + */ + +namespace FireflyIII\Support\Search; + +use Carbon\Carbon; +use FireflyIII\Exceptions\FireflyException; +use FireflyIII\Helpers\Collector\GroupCollectorInterface; +use FireflyIII\Models\AccountType; +use FireflyIII\Repositories\Account\AccountRepositoryInterface; +use FireflyIII\Repositories\Bill\BillRepositoryInterface; +use FireflyIII\Repositories\Budget\BudgetRepositoryInterface; +use FireflyIII\Repositories\Category\CategoryRepositoryInterface; +use FireflyIII\Repositories\Tag\TagRepositoryInterface; +use FireflyIII\User; +use Gdbots\QueryParser\Node\Field; +use Gdbots\QueryParser\Node\Node; +use Gdbots\QueryParser\Node\Word; +use Gdbots\QueryParser\ParsedQuery; +use Gdbots\QueryParser\QueryParser; +use Illuminate\Pagination\LengthAwarePaginator; +use Illuminate\Support\Collection; +use Log; + +/** + * Class BetterQuerySearch + * @package FireflyIII\Support\Search + */ +class BetterQuerySearch implements SearchInterface +{ + private AccountRepositoryInterface $accountRepository; + private BillRepositoryInterface $billRepository; + private BudgetRepositoryInterface $budgetRepository; + private CategoryRepositoryInterface $categoryRepository; + private TagRepositoryInterface $tagRepository; + private User $user; + private ParsedQuery $query; + private int $page; + private array $words; + private array $validOperators; + private GroupCollectorInterface $collector; + private float $startTime; + private Collection $modifiers; + + public function __construct() + { + $this->modifiers = new Collection; + $this->page = 1; + $this->words = []; + $this->validOperators = config('firefly.search_modifiers'); + $this->startTime = microtime(true); + $this->accountRepository = app(AccountRepositoryInterface::class); + $this->categoryRepository = app(CategoryRepositoryInterface::class); + $this->budgetRepository = app(BudgetRepositoryInterface::class); + $this->billRepository = app(BillRepositoryInterface::class); + $this->tagRepository = app(TagRepositoryInterface::class); + } + + /** + * @inheritDoc + */ + public function getModifiers(): Collection + { + return $this->modifiers; + } + + /** + * @inheritDoc + */ + public function getWordsAsString(): string + { + return implode(' ', $this->words); + } + + /** + * @inheritDoc + */ + public function setPage(int $page): void + { + $this->page = $page; + } + + /** + * @inheritDoc + */ + public function hasModifiers(): bool + { + // TODO: Implement hasModifiers() method. + die(__METHOD__); + } + + /** + * @inheritDoc + */ + public function parseQuery(string $query) + { + $parser = new QueryParser(); + $this->query = $parser->parse($query); + + // get limit from preferences. + $pageSize = (int) app('preferences')->getForUser($this->user, 'listPageSize', 50)->data; + $this->collector = app(GroupCollectorInterface::class); + $this->collector->setLimit($pageSize)->setPage($this->page)->withAccountInformation()->withCategoryInformation()->withBudgetInformation(); + + foreach ($this->query->getNodes() as $searchNode) { + $this->handleSearchNode($searchNode); + } + + $this->collector->setSearchWords($this->words); + + } + + /** + * @inheritDoc + */ + public function searchTime(): float + { + return microtime(true) - $this->startTime; + } + + /** + * @inheritDoc + */ + public function searchTransactions(): LengthAwarePaginator + { + return $this->collector->getPaginatedGroups(); + } + + /** + * @inheritDoc + */ + public function setUser(User $user): void + { + $this->user = $user; + $this->accountRepository->setUser($user); + $this->billRepository->setUser($user); + $this->categoryRepository->setUser($user); + $this->budgetRepository->setUser($user); + } + + /** + * @param Node $searchNode + * @throws FireflyException + */ + private function handleSearchNode(Node $searchNode): void + { + $class = get_class($searchNode); + switch ($class) { + default: + throw new FireflyException(sprintf('Firefly III search cant handle "%s"-nodes', $class)); + case Word::class: + $this->words[] = $searchNode->getValue(); + break; + case Field::class: + /** @var Field $searchNode */ + // used to search for x:y + $operator = $searchNode->getValue(); + $value = $searchNode->getNode()->getValue(); + // must be valid operator: + if (in_array($operator, $this->validOperators, true)) { + $this->updateCollector($operator, $value); + $this->modifiers->push([ + 'type' => $operator, + 'value' => $value, + ]); + } + break; + } + + } + + /** + * @param string $operator + * @param string $value + */ + private function updateCollector(string $operator, string $value): void + { + $allAccounts = new Collection; + switch ($operator) { + default: + die(sprintf('Unsupported search operator: "%s"', $operator)); + case 'from': + case 'source': + // source can only be asset, liability or revenue account: + $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::REVENUE]; + $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25); + if ($accounts->count() > 0) { + $allAccounts = $accounts->merge($allAccounts); + } + $this->collector->setSourceAccounts($allAccounts); + break; + case 'to': + case 'destination': + // source can only be asset, liability or expense account: + $searchTypes = [AccountType::ASSET, AccountType::MORTGAGE, AccountType::LOAN, AccountType::DEBT, AccountType::EXPENSE]; + $accounts = $this->accountRepository->searchAccount($value, $searchTypes, 25); + if ($accounts->count() > 0) { + $allAccounts = $accounts->merge($allAccounts); + } + $this->collector->setDestinationAccounts($allAccounts); + break; + case 'category': + $result = $this->categoryRepository->searchCategory($value, 25); + if ($result->count() > 0) { + $this->collector->setCategories($result); + } + break; + case 'bill': + $result = $this->billRepository->searchBill($value, 25); + if ($result->count() > 0) { + $this->collector->setBills($result); + } + break; + case 'tag': + $result = $this->tagRepository->searchTag($value); + if ($result->count() > 0) { + $this->collector->setTags($result); + } + break; + case 'budget': + $result = $this->budgetRepository->searchBudget($value, 25); + if ($result->count() > 0) { + $this->collector->setBudgets($result); + } + break; + case 'amount_is': + case 'amount': + $amount = app('steam')->positive((string) $value); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); + $this->collector->amountIs($amount); + break; + case 'amount_max': + case 'amount_less': + $amount = app('steam')->positive((string) $value); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); + $this->collector->amountLess($amount); + break; + case 'amount_min': + case 'amount_more': + $amount = app('steam')->positive((string) $value); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $amount)); + $this->collector->amountMore($amount); + break; + case 'type': + $this->collector->setTypes([ucfirst($value)]); + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + break; + case 'date': + case 'on': + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + $start = new Carbon($value); + $this->collector->setRange($start, $start); + break; + case 'date_before': + case 'before': + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + $before = new Carbon($value); + $this->collector->setBefore($before); + break; + case 'date_after': + case 'after': + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + $after = new Carbon($value); + $this->collector->setAfter($after); + break; + case 'created_on': + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + $createdAt = new Carbon($value); + $this->collector->setCreatedAt($createdAt); + break; + case 'updated_on': + Log::debug(sprintf('Set "%s" using collector with value "%s"', $operator, $value)); + $updatedAt = new Carbon($value); + $this->collector->setUpdatedAt($updatedAt); + break; + case 'external_id': + $this->collector->setExternalId($value); + break; + case 'internal_reference': + $this->collector->setInternalReference($value); + break; + } + } +} \ No newline at end of file diff --git a/app/Support/Search/Search.php b/app/Support/Search/Search.php index 56f9f8a6bc..3a97896476 100644 --- a/app/Support/Search/Search.php +++ b/app/Support/Search/Search.php @@ -145,7 +145,7 @@ class Search implements SearchInterface Log::debug('Start of searchTransactions()'); // get limit from preferences. - $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; + $pageSize = (int) app('preferences')->get('listPageSize', 50)->data; /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); @@ -156,9 +156,7 @@ class Search implements SearchInterface // Most modifiers can be applied to the collector directly. $collector = $this->applyModifiers($collector); - return $collector->getPaginatedGroups(); - } /** @@ -181,10 +179,6 @@ class Search implements SearchInterface */ private function applyModifiers(GroupCollectorInterface $collector): GroupCollectorInterface { - /* - * TODO: - * 'bill'? - */ $totalAccounts = new Collection; foreach ($this->modifiers as $modifier) { @@ -199,6 +193,7 @@ class Search implements SearchInterface if ($accounts->count() > 0) { $totalAccounts = $accounts->merge($totalAccounts); } + $collector->setSourceAccounts($totalAccounts); break; case 'to': case 'destination': @@ -208,6 +203,7 @@ class Search implements SearchInterface if ($accounts->count() > 0) { $totalAccounts = $accounts->merge($totalAccounts); } + $collector->setDestinationAccounts($totalAccounts); break; case 'category': $result = $this->categoryRepository->searchCategory($modifier['value'], 25); @@ -227,7 +223,6 @@ class Search implements SearchInterface $collector->setTags($result); } break; - break; case 'budget': $result = $this->budgetRepository->searchBudget($modifier['value'], 25); if ($result->count() > 0) { @@ -292,7 +287,6 @@ class Search implements SearchInterface break; } } - $collector->setAccounts($totalAccounts); return $collector; } diff --git a/resources/views/v1/search/index.twig b/resources/views/v1/search/index.twig index 9593b0f272..41089ab394 100644 --- a/resources/views/v1/search/index.twig +++ b/resources/views/v1/search/index.twig @@ -30,9 +30,11 @@ + {% if '' != query %}

{{ trans('firefly.search_for_query', {query: query|escape})|raw}}

+ {% endif %} {% if modifiers|length > 0 %}

{{ trans('firefly.modifiers_applies_are') }}