Introduce visibility filters - #165
Open
nilmerg wants to merge 9 commits into
Open
Conversation
nilmerg
force-pushed
the
feature/visibility-filters-76
branch
from
July 23, 2026 13:32
8b02f63 to
3a63142
Compare
nilmerg
force-pushed
the
feature/visibility-filters-76
branch
from
July 24, 2026 12:56
3a63142 to
4b9bf83
Compare
nilmerg
force-pushed
the
feature/visibility-filters-76
branch
from
July 24, 2026 14:29
4b9bf83 to
c5bde13
Compare
Visibility filters are supposed to constrain a model's query result at all times. Be it by selecting from it or joining it. Usecases such as soft-deletions are thus made possible to guarantee that ORM queries never fetch related rows.
Relation filters are supposed to constrain joined or eager loaded results, but only given the direction they were declared in. For example, if the opposite relation does not declare the same or any filter at all, joining from this side does not constrain the result the same way as the other way round.
…tic` This is the equivalent of `Relation::setFilter()` but for the junction table.
…ubjects): Filter\Chain` Utility method to qualify a simple filter using only base table columns (e.g. `'base.column'`)
…el $source, Model $target): void` Supposed to validate and resolve relation filters so that they reference a model which `qualifyFilter` can work with.
Provides access to a model's visibility filter and resolves it as well, similar to what `resolveRelationFilter` does but for relation filters.
* `getSelectBase` applies the base model's visibility filters. * `assembleSelect` applies visibility and relation filters to joins. This introduces a compatibility related change to `Relation::resolve()` which now yields a key and extending classes need to re-yield it. `BelongsToMany` is such a case and did that already for a long time. A deprecation notice will appear if a relation still yields an int. * `derive` applies filters of the relation that is eagerly loaded as otherwise only visibility filters would apply. * `createSubQuery` overrides the filters of the reversed relations with those of the original path, preserving the semantics as if the results were joined normally.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
nilmerg
force-pushed
the
feature/visibility-filters-76
branch
from
July 28, 2026 11:08
c5bde13 to
f7a4ce3
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Adds a way to always constrain query results, independent of the filters a caller supplies — useful for row-level access rules (soft-deletes, tenant scoping, …). Two kinds of constraint are introduced:
Model::createVisibilityFilter(Filter\Chain $filter). References the model's own columns only. Applied to the base model as aWHEREclause and, whenever the model is joined, added to theJOINcondition.Relation::setFilter(Filter\Rule $filter), plusBelongsToMany::setThroughFilter()for the junction. Applied as extraJOINconditions. A relation filter may reference either the source or the target table by its alias (target by default), and the through filter the source or junction table.Filter columns must be actual columns of the referenced table and comparison values are passed as-is to the query builder (behaviors are not applied);
ExpressionInterfacevalues are supported under the same rules.How it works
Qualification happens in two stages so a relation filter can be applied unchanged regardless of join direction:
Resolver::resolveRelationFilter()(at relation-resolution time) rewrites each column to a logical alias — the source's or target's table alias.Resolver::qualifyFilter(Filter\Chain, Model ...$subjects)(at assembly time) maps those logical aliases to the runtime aliases, with both participating models in scope.Because the filter addresses tables by alias rather than by role, it resolves correctly when the
FilterProcessorreverses a join into a correlated sub-query (filtering on a to-many relation) and whenderive()lazy-loads a relation — the source and target simply swap places andqualifyFilterstill finds them.Changes
Model—createVisibilityFilter()hook (no-op by default).Relation—setFilter()/getFilter();resolve()now yields the relation to join as the generator key (a numeric key still works, with a deprecation notice).BelongsToMany—setThroughFilter()/getThroughFilter(); propagates the through/relation filters and the join type to the junction and target joins.Resolver—getVisibilityFilter()(resolve + cache a model's filter),resolveRelationFilter()andqualifyFilter(); resolves a relation's filters when resolving the relation.Query— applies the visibility filter to the baseWHEREand to joined models, and relation/through filters to theirJOINconditions; carries relation filters into reversed sub-queries andderive().Tests
Relation/BelongsToManyaccessors and forResolver::resolveRelationFilter()/qualifyFilter()/getVisibilityFilter()(target- and source-referencing columns, alias/column validation, junction handling, deep-clone).VisibilityFilterTestfor baseWHERE, joined model visibility filters, relation and through filters, a relation filter referencing the source table, and — via theFilterProcessor— the same filters propagating unchanged through the reversed joins of a sub-query.FilterProcessorTestruns against MySQL and PostgreSQL.🤖 Generated with Claude Code