Filters#
Composable conditions over the QueryBuilder. app/Core/Utils/Filters/.
This is the only way anything is queried in this system. A presenter or a component writes no SQL — it assembles an array of filters and hands it to a manager.
$items = $this->newsManager->findAll(
filters: [
new EqualFilter('active', true),
new LikeFilter('name', $searched),
],
orderBy: ['published' => false], // false = DESC!
limit: 20,
)->execute();
The catalogue#
There are 33 filters in the system (ls app/Core/Utils/Filters/*.php). The most used
ones:
| Filter | What it does |
|---|---|
EqualFilter, NotEqualFilter |
equality / inequality |
InFilter, NotInFilter |
the value is (not) in a list |
LikeFilter, NotLikeFilter |
a partial match |
IsNullFilter, IsNotNullFilter |
an empty / non-empty value |
GreaterThanFilter, GreaterOrEqualFilter, LessThanFilter, LessOrEqualFilter |
comparisons |
BetweenFilter, NotBetweenFilter, RangeFilter, RangeInclusiveFilter |
ranges |
DateEqualFilter, DateBeforeFilter, DateAfterFilter, DateBetweenFilter, DateRangeFilter |
date variants |
FulltextFilter |
full text over the database |
SearchIdFilter |
ids returned by Elasticsearch |
DistanceFilter |
geographic distance |
ParameterRelFilter, ParameterRelLikeFilter, ParameterRelRangeFilter |
the dynamic parameters of classifieds and the company catalogue |
Composition and parentheses#
| Filter | Meaning |
|---|---|
AndFilter |
every condition |
OrFilter |
any condition |
GroupFilter |
a parenthesis |
Without GroupFilter an OR spills into the whole query
A AND (B OR C) without the parenthesis becomes A AND B OR C — a different thing in
SQL, because AND binds more tightly. The query does not fail, it just returns more
rows than it should.
Dotted paths across relations#
The necessary joins are added by JoinHelper itself, and it keeps its aliases per
QueryBuilder — which matters, because a single query has several QueryBuilders side by
side: the main one, a separate one for computeTotalCount(), and clones that GroupFilter
makes for every sub-filter.
A dotted path across a COLLECTION multiplies rows
A filter over a ToMany relation creates a join that returns one record several times. On a listing with a count that skews the numbers — pagination then claims "137 items" where there are fifty. It does not crash and the first page looks right.
The filter order gets rearranged#
Before the query is assembled, FilterSortHelper::sortFilters() reorders the filters by
the column order in the entity and expands a multi-field filter into individual ones. It
is called from GetAllTrait and from TotalCountTrait, so both queries come out alike.
This is not result ordering
Result ordering is handled by orderBy and OrderByHelper — a different thing in a
different file.
Ordering the results#
The direction is a BOOL, not a string
OrderByHelper does literally $dir = $direction ? 'ASC' : 'DESC'. So
['id' => 'DESC'] sorts ASCENDING, because a non-empty string is truthy. Nothing
crashes and the listing looks sorted — just the other way round, so "the last ten"
shows the ten oldest.
(In the DataGrid it is different: setDefaultOrder() takes a
string and converts it itself.)
Procedure: writing my own filter#
- Go through the catalogue first. Most needs are covered; a new filter makes sense for a domain concept, not for yet another comparison variant.
- Implement
App\Core\Utils\Filters\Interfaces\Filter—toSql(),getBindings(),apply()and the alias setters. - Start from the closest existing filter in the same folder; the shape is settled.
- When the filter must not be cached, mark it
NonCacheableFilter. - Verify it in the count too. A filter is applied to the main query and to the query computing the total — when those two diverge, pagination shows nonsense.
NonCacheableFilter is a marker for queries that must not be cached
SearchIdFilter uses it: a list of ids from Elasticsearch would produce a unique cache
key for every search term (filling up Redis), and above all it would create a second
truth about the list next to the ES index. In detail: Cache.
Filters in the administration and on the front end#
| Where | How they arise |
|---|---|
| DataGrid | from the column filters plus addDefaultFilter() |
| front-end listings | from URL parameters through the filter component |
| e-shop facets | their own rules — counts are narrowed by everything except the facet itself |
A front-end range filter needs a round trip through the URL
Without it the value is lost between requests and the filter silently does not filter — it returns everything while looking as if it were set.
Where to look#
| I want | Where |
|---|---|
| the filter catalogue | app/Core/Utils/Filters/ |
| the filter interface | app/Core/Utils/Filters/Interfaces/Filter.php |
| the "do not cache" marker | …/Interfaces/NonCacheableFilter.php |
| adding the joins | app/Core/Utils/Helpers/Filters/JoinHelper.php |
| rearranging the filters | app/Core/Utils/Helpers/Filters/FilterSortHelper.php |
| the sort direction | app/Core/Utils/Helpers/Sqls/OrderByHelper.php |
Follow-up chapters: Working with tables · Cache · Elasticsearch · Calling the API