Skip to content

Filter by a relation path in Filter::related(), and fix criteria path errors - #9

Merged
SoloPHP merged 2 commits into
mainfrom
feat/related-path
Nov 10, 2026
Merged

SoloPHP merged 2 commits into
mainfrom
feat/related-path

Conversation

@SoloPHP

@SoloPHP SoloPHP commented Nov 10, 2026

Copy link
Copy Markdown
Owner

Prepares v1.5.0: Filter::related() takes a relation path, two errors of criteria paths from 1.4.0 are fixed, and the documentation says what findOrFail() answers as not found.

Changes

  • Filter::related() takes a dotted relation path, such as tags.category, through relations of any kind, and nests one related filter per relation: related('tags.category', $criteria, $visibility) is related('tags', related('category', $criteria, $visibility)). As in a Load path, the criteria and visibility belong to the last relation, and the relations before it see their active rows; nested calls still give them conditions or a visibility of their own. The path is expanded when the filter is built, so the compiler is unchanged and sees the hand-written nesting. A path in a criteria key stays BelongsTo-only.
  • A criteria path whose first segment is a number, such as ['1.code' => ...], threw a TypeError: PHP keeps the segment as an integer key of the array that groups the conditions of one target. It is now rejected with InvalidArgumentException, as any other unknown relation.
  • A write whose criteria name a path through an unknown relation said the relation is unknown, where a path through a known one said that paths are for reads. The check for a read now comes before the relation is looked up (PredicateCompiler::assertReadQuery()), so both say the latter.
  • Docs: what findOrFail() answers as not found. It is an ID no visible row has, or one the type of the key rejects, such as "007" for an integer key; the docs said "an ID no row can have", which the package cannot know. An ID that only the server rejects stays the driver's exception. On PostgreSQL 17 these were checked on the server: a number beyond the range of an INTEGER, SMALLINT or BIGINT column (22003), a string that is not a UUID for a uuid column (22P02) and broken UTF-8 (22021); MySQL 8.4 answers all of them as not found. The mapped type does not tell what the column holds (INT UNSIGNED, a uuid column mapped as string), so the package does not guess; where a route can carry such an ID, its parameter is constrained.
  • The API reference names the version it describes, 1.5, where it still said 1.2. Changelog, docs version labels and the dev-main branch alias are those of 1.5.0.

Behavior changes for 1.4 users

  • Filter::related() with a dotted name was rejected as "a plain relation name" is required; it is a path now. An invalid name reports "A related filter requires a relation name or a dotted relation path."
  • The two fixes change an error only: TypeError becomes InvalidArgumentException, and "Unknown mapped relation" in a write becomes "Related filters and relation paths are supported only for read queries."

Verification

  • 753 tests on PHP 8.4, PHPStan and PHPCS; examples/crud.php, translations.php and relations.php run.
  • Integration suite on PostgreSQL 17, MySQL 8.4 and MariaDB 12.3, 44 tests on each, on PHP 8.3.
  • New tests: relation paths through BelongsTo, BelongsToMany and three relations, equal to the nested calls in value and SQL; the visibility of the last relation; criteria paths that start and end with a number; writes with a path or a related filter on an unknown relation. The two fix tests fail without the fixes.
  • Not run locally: the documentation build, and the CI matrix beyond the combination above (PHP 8.5, DBAL 4.3.*).

🤖 Generated with Claude Code

`Filter::related()` takes a dotted relation path, such as
`tags.category`, and nests one related filter per relation:
`related('tags.category', $criteria, $visibility)` is
`related('tags', related('category', $criteria, $visibility))`. As in
a `Load` path, the criteria and visibility belong to the last relation,
and the relations before it see their active rows; nested calls still
give them conditions or a visibility of their own. The path is expanded
when the filter is built, so the compiler and the comparison of load
settings see the same value as the hand-written nesting.

Unlike a path in a criteria key, which stays BelongsTo-only, the path
goes through to-many relations: all conditions of the call match one
row of the last target. Empty segments and SQL fragments are rejected
as before.

Tests cover paths through BelongsTo, BelongsToMany and three relations,
equal to the nested calls in value and SQL, the visibility of the last
relation, and a path in the nested-locale test on the real databases.
A criteria path whose first segment is a number, such as `1.code`, threw
a TypeError: PHP keeps the segment as an integer key of the array that
groups the conditions of one target. It is an unknown relation, as any
other name no relation has, and is rejected with
InvalidArgumentException.

A write whose criteria name a path through an unknown relation said the
relation is unknown, where a path through a known one said that paths
are for reads. The check for a read now comes before the relation is
looked up, so both say the latter.

findOrFail() is described by what it does: it answers as not found an
ID that the type of the key rejects, such as "007" for an integer key,
not every ID no row can have. What the column holds is known to the
server only, so an ID that only the server rejects stays its error: on
PostgreSQL, a number beyond the column's range, or a string that is not
a UUID for a uuid column. The API reference names the version it
describes, 1.5, where it still said 1.2.

Tests cover paths that start and end with a number, and writes with a
path or a related filter on an unknown relation.
@SoloPHP
SoloPHP merged commit 93b9631 into main Nov 10, 2026
26 checks passed
@SoloPHP
SoloPHP deleted the feat/related-path branch November 10, 2026 03:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant