Add SelectQuery::reshape() for type-tracked result reshaping#19484
Draft
dereuromark wants to merge 1 commit into
Draft
Add SelectQuery::reshape() for type-tracked result reshaping#19484dereuromark wants to merge 1 commit into
dereuromark wants to merge 1 commit into
Conversation
7e254e3 to
8bd8f36
Compare
8bd8f36 to
b7ee889
Compare
find()->first() is statically typed as the entity even when a formatter reshapes each row, so reading the result of find(...)->first() gets a type that lies at runtime and hides the reshape from the call site. reshape() makes the reshape explicit and rebinds the result generic, so first()/firstOrFail()/all() resolve to the new shape instead of the entity. Runtime behavior is identical to formatResults(..., OVERWRITE); the only addition is the honest static type via a TNew template bound to EntityInterface or array. Named reshape() rather than map() to avoid confusion with the collection map(), which applies per element and returns a collection; this operates on the whole result set and returns the query.
b7ee889 to
31cbdb7
Compare
ADmad
reviewed
May 30, 2026
| */ | ||
| public function reshape(Closure $callback): SelectQuery | ||
| { | ||
| $this->formatResults($callback, self::OVERWRITE); |
Member
There was a problem hiding this comment.
Using the overwrite mode means this method can only be used once? Or rather if used multiple times only the last call will work.
This means it can't be used inside custom finders as if you chain finders like $table->find('foo')->find('bar') and both the finders use reshape(), the callback set by findFoo() will get overwritten.
If finder stacking can't be used then I question the need for this method. One can just do $table->find('foo')->find('bar')->all()->somecollectionMethd() instead.
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.
Draft. Adds
SelectQuery::map()— an explicit, type-tracked result-reshaping method. First concrete step from the find()-honesty discussion in #19482.Problem
$x = $table->find('auth')->first()is statically typed as the entity even when a finder reshapes each row viaformatResults(..., OVERWRITE). The static type lies at runtime, and the reshape is invisible at the assignment site.What this adds
map(Closure $mapper): SelectQuery<TNew>rebinds the result generic to the mapped shape, sofirst()/firstOrFail()/all()resolve to the new shape instead of the entity. Runtime behavior is identical to an OVERWRITE formatter — the only addition is the honest static type.Inferred types (PHPStan level 8):
find('all')->first()EntityInterface|nullfind('all')->map(...)SelectQuery<array{id: int}>find('all')->map(...)->first()array{id: int}|nullfind('all')->map(...)->all()ResultSetInterface<..., array{id: int}>find('all')->map(closure returning int)intnot in theEntityInterface|arraybound)Implementation note:
map()mutates$this(overwrite formatter) but returns a different generic; the same-instance rebind goes through a tiny private helper typed at the class bound, the same narrowing idiomTable::find()already uses. No baseline change, no PHPStan extension.Out of scope (need design discussion first — see #19482)
projectAs()(DTO projection, shipped in 5.3) — needs a bound decision; a bareobjectbound was measured to break 24 unrelated spots.formatResults(..., OVERWRITE)in favor ofmap().find('list'),find('threaded'), ...).