Skip to content

Add SelectQuery::reshape() for type-tracked result reshaping#19484

Draft
dereuromark wants to merge 1 commit into
5.nextfrom
find-honesty-5next
Draft

Add SelectQuery::reshape() for type-tracked result reshaping#19484
dereuromark wants to merge 1 commit into
5.nextfrom
find-honesty-5next

Conversation

@dereuromark
Copy link
Copy Markdown
Member

@dereuromark dereuromark commented May 30, 2026

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 via formatResults(..., 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, so first() / 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.

$rows = $articles->find()
    ->map(fn($results) => $results->map(fn($row) => $row->toArray()))
    ->all();

Inferred types (PHPStan level 8):

Expression Inferred type
find('all')->first() EntityInterface|null
find('all')->map(...) SelectQuery<array{id: int}>
find('all')->map(...)->first() array{id: int}|null
find('all')->map(...)->all() ResultSetInterface<..., array{id: int}>
find('all')->map(closure returning int) rejected (int not in the EntityInterface|array bound)

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 idiom Table::find() already uses. No baseline change, no PHPStan extension.

Out of scope (need design discussion first — see #19482)

  • Honest typing for projectAs() (DTO projection, shipped in 5.3) — needs a bound decision; a bare object bound was measured to break 24 unrelated spots.
  • Soft-deprecating shape-changing formatResults(..., OVERWRITE) in favor of map().
  • Typed entries for built-in reshaping finders (find('list'), find('threaded'), ...).

@dereuromark dereuromark force-pushed the find-honesty-5next branch from 7e254e3 to 8bd8f36 Compare May 30, 2026 15:04
@dereuromark dereuromark added this to the 5.4.0 milestone May 30, 2026
@dereuromark dereuromark force-pushed the find-honesty-5next branch from 8bd8f36 to b7ee889 Compare May 30, 2026 15:29
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.
@dereuromark dereuromark force-pushed the find-honesty-5next branch from b7ee889 to 31cbdb7 Compare May 30, 2026 16:00
@dereuromark dereuromark changed the title Add SelectQuery::map() for type-tracked result reshaping Add SelectQuery::reshape() for type-tracked result reshaping May 30, 2026
*/
public function reshape(Closure $callback): SelectQuery
{
$this->formatResults($callback, self::OVERWRITE);
Copy link
Copy Markdown
Member

@ADmad ADmad May 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants