baseQuery($user)->count(); } public function countMatching(User $user, string $field, string $operator, string $token): int { $query = $this->matchQuery($user, $field, $operator, $token); return $query === null ? 0 : $query->count(); } public function matching(User $user, string $field, string $operator, string $token, ?int $limit = null): Collection { $query = $this->matchQuery($user, $field, $operator, $token); if ($query === null) { return new Collection; } $query->orderByDesc('transaction_date')->orderByDesc('id'); if ($limit !== null) { $query->limit($limit); } return $query->get(); } public function countMatchingAny(User $user, array $conditions): int { $query = $this->anyQuery($user, $conditions); return $query === null ? 0 : $query->count(); } public function matchingAny(User $user, array $conditions, ?int $limit = null): Collection { $query = $this->anyQuery($user, $conditions); if ($query === null) { return new Collection; } $query->orderByDesc('transaction_date')->orderByDesc('id'); if ($limit !== null) { $query->limit($limit); } return $query->get(); } /** * @return Builder */ private function baseQuery(User $user): Builder { return Transaction::query() ->where('user_id', $user->id) ->whereNull('category_id') ->whereNull('description_iv'); } /** * @return Builder|null */ private function matchQuery(User $user, string $field, string $operator, string $token): ?Builder { if (! in_array($field, self::ALLOWED_FIELDS, true) || trim($token) === '') { return null; } $query = $this->baseQuery($user); $token = mb_strtolower(trim($token)); if ($operator === 'equals') { return $query->whereRaw("LOWER({$field}) = ?", [$token]); } return $query->whereRaw("LOWER({$field}) LIKE ?", ['%'.$this->escapeLike($token).'%']); } /** * Build a single query matching ANY of the conditions (OR). Invalid * conditions (unknown field or blank token) are skipped; returns null when * none remain. * * @param list $conditions * @return Builder|null */ private function anyQuery(User $user, array $conditions): ?Builder { $valid = array_values(array_filter( $conditions, fn (array $condition): bool => in_array($condition['field'], self::ALLOWED_FIELDS, true) && trim($condition['token']) !== '', )); if ($valid === []) { return null; } return $this->baseQuery($user)->where(function (Builder $builder) use ($valid): void { foreach ($valid as $condition) { $field = $condition['field']; $token = mb_strtolower(trim($condition['token'])); if ($condition['operator'] === 'equals') { $builder->orWhereRaw("LOWER({$field}) = ?", [$token]); continue; } $builder->orWhereRaw("LOWER({$field}) LIKE ?", ['%'.$this->escapeLike($token).'%']); } }); } private function escapeLike(string $value): string { return str_replace(['\\', '%', '_'], ['\\\\', '\\%', '\\_'], $value); } }