. */ declare(strict_types=1); namespace FireflyIII\Support\Search; use Carbon\Carbon; 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\User; use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Support\Collection; use Log; /** * Class Search. */ class Search implements SearchInterface { /** @var AccountRepositoryInterface */ private $accountRepository; /** @var BillRepositoryInterface */ private $billRepository; /** @var BudgetRepositoryInterface */ private $budgetRepository; /** @var CategoryRepositoryInterface */ private $categoryRepository; /** @var int */ private $limit = 100; /** @var Collection */ private $modifiers; /** @var string */ private $originalQuery = ''; /** @var float */ private $startTime; /** @var User */ private $user; /** @var array */ private $validModifiers; /** @var array */ private $words = []; /** * Search constructor. */ public function __construct() { $this->modifiers = new Collection; $this->validModifiers = (array)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); if ('testing' === config('app.env')) { Log::warning(sprintf('%s should not be instantiated in the TEST environment!', get_class($this))); } } /** * @return Collection */ public function getModifiers(): Collection { return $this->modifiers; } /** * @return string */ public function getWordsAsString(): string { $string = implode(' ', $this->words); if ('' === $string) { return \is_string($this->originalQuery) ? $this->originalQuery : ''; } return $string; } /** * @return bool */ public function hasModifiers(): bool { return $this->modifiers->count() > 0; } /** * @param string $query */ public function parseQuery(string $query): void { $filteredQuery = $query; $this->originalQuery = $query; $pattern = '/[a-z_]*:[0-9a-z-.]*/i'; $matches = []; preg_match_all($pattern, $query, $matches); foreach ($matches[0] as $match) { $this->extractModifier($match); $filteredQuery = str_replace($match, '', $filteredQuery); } $filteredQuery = trim(str_replace(['"', "'"], '', $filteredQuery)); if ('' !== $filteredQuery) { $this->words = array_map('trim', explode(' ', $filteredQuery)); } } /** * @param string $string */ private function extractModifier(string $string): void { $parts = explode(':', $string); if (2 === count($parts) && '' !== trim((string)$parts[1]) && '' !== trim((string)$parts[0])) { $type = trim((string)$parts[0]); $value = trim((string)$parts[1]); if (in_array($type, $this->validModifiers, true)) { // filter for valid type $this->modifiers->push(['type' => $type, 'value' => $value]); } } } /** * @return float */ public function searchTime(): float { return microtime(true) - $this->startTime; } /** * @return LengthAwarePaginator */ public function searchTransactions(): LengthAwarePaginator { Log::debug('Start of searchTransactions()'); $pageSize = 50; $page = 1; /** @var GroupCollectorInterface $collector */ $collector = app(GroupCollectorInterface::class); $collector->setLimit($pageSize)->setPage($page)->withAccountInformation(); $collector->withCategoryInformation()->withBudgetInformation(); $collector->setSearchWords($this->words); // Most modifiers can be applied to the collector directly. $collector = $this->applyModifiers($collector); return $collector->getPaginatedGroups(); } /** * @param GroupCollectorInterface $collector * * @return GroupCollectorInterface * @SuppressWarnings(PHPMD.CyclomaticComplexity) */ private function applyModifiers(GroupCollectorInterface $collector): GroupCollectorInterface { /* * TODO: * 'bill', */ $totalAccounts = new Collection; foreach ($this->modifiers as $modifier) { switch ($modifier['type']) { default: die(sprintf('unsupported modifier: "%s"', $modifier['type'])); 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($modifier['value'], $searchTypes); if ($accounts->count() > 0) { $totalAccounts = $accounts->merge($totalAccounts); } 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($modifier['value'], $searchTypes); if ($accounts->count() > 0) { $totalAccounts = $accounts->merge($totalAccounts); } break; case 'category': $result = $this->categoryRepository->searchCategory($modifier['value']); if ($result->count() > 0) { $collector->setCategories($result); } break; case 'bill': $result = $this->billRepository->searchBill($modifier['value']); if ($result->count() > 0) { $collector->setBills($result); } break; case 'budget': $result = $this->budgetRepository->searchBudget($modifier['value']); if ($result->count() > 0) { $collector->setBudgets($result); } break; case 'amount_is': case 'amount': $amount = app('steam')->positive((string)$modifier['value']); Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $amount)); $collector->amountIs($amount); break; case 'amount_max': case 'amount_less': $amount = app('steam')->positive((string)$modifier['value']); Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $amount)); $collector->amountLess($amount); break; case 'amount_min': case 'amount_more': $amount = app('steam')->positive((string)$modifier['value']); Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $amount)); $collector->amountMore($amount); break; case 'type': $collector->setTypes([ucfirst($modifier['value'])]); Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $modifier['value'])); break; case 'date': case 'on': Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $modifier['value'])); $start = new Carbon($modifier['value']); $collector->setRange($start, $start); break; case 'date_before': case 'before': Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $modifier['value'])); $before = new Carbon($modifier['value']); $collector->setBefore($before); break; case 'date_after': case 'after': Log::debug(sprintf('Set "%s" using collector with value "%s"', $modifier['type'], $modifier['value'])); $after = new Carbon($modifier['value']); $collector->setAfter($after); break; } } $collector->setAccounts($totalAccounts); return $collector; } /** * @param int $limit */ public function setLimit(int $limit): void { $this->limit = $limit; } /** * @param User $user */ 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); } }