Move result projection onto Table (Table::projectAs/list); drop SelectQuery::reshape() by dereuromark · Pull Request #19484 · cakephp/cakephp · GitHub
Skip to content

Move result projection onto Table (Table::projectAs/list); drop SelectQuery::reshape()#19484

Draft
dereuromark wants to merge 2 commits into
5.nextfrom
find-honesty-5next
Draft

Move result projection onto Table (Table::projectAs/list); drop SelectQuery::reshape()#19484
dereuromark wants to merge 2 commits into
5.nextfrom
find-honesty-5next

Conversation

@dereuromark

@dereuromark dereuromark commented May 30, 2026

Copy link
Copy Markdown
Member

Draft. Part of 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.

Direction (revised)

Rather than adding result-reshaping methods to SelectQuery, this keeps SelectQuery a query builder (it returns entities or arrays) and puts shape-changing transforms on the Table (repository), where the honest return type can be a concrete array. This follows the same trajectory as disableHydration() -> Table::unhydratedFind()/UnhydratedSelectQuery.

It also avoids the overwrite-formatter stacking limitation: the terminal repository methods consume a query and return an array instead of mutating a chainable formatter.

What this adds

  • Table::projectAs(class-string $class, ?SelectQuery $query = null): array returns a concrete list<T> of DTOs. Delegates to the existing query projection path, so rows are mapped from the raw selected columns (entity visibility/accessors do not affect the DTO data).
  • Table::list(?SelectQuery $query = null, ...): array is the terminal counterpart to the list finder and returns the combined array directly.
  • Removes SelectQuery::reshape() (never released).
  • Soft-deprecates SelectQuery::projectAs() (doc-only) in favor of Table::projectAs().
$dtos = $articles->projectAs(ArticleDto::class);

$options = $articles->list(
    $articles->find()->where(['published' => true]),
    valueField: 'title',
);

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

  • A shared bound so createFromArray()-style DTOs are type-tracked too.
  • Removing SelectQuery::formatResults()/projectAs() in 6.0, which needs an internal result-decorator pipeline first (find('list')/find('threaded')/associations still use formatResults() internally).

@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
Comment thread src/ORM/Query/SelectQuery.php Outdated

@ADmad ADmad May 30, 2026

Copy link
Copy Markdown
Member

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.

@LordSimal

Copy link
Copy Markdown
Contributor

I am fine with this as it is.

Looking at our docs we only mention formatResults() to be used for adding calculated fields even though it can also be used for other stuff as well.

But I know this is primarily focused on getting the static typing move more forward, so we just have to make sure we document this new method properly so users understand why it exsist, when to use it, and when to user alternatives.

@dereuromark

Copy link
Copy Markdown
Member Author

Based on the open topics above I think we need to first talk more about the topic itself in the parent issue ( #19482 ).

Pivots the find-honesty work from augmenting the query class to terminal
repository methods, per review feedback: SelectQuery stays a query builder
that yields entities or arrays, and shape-changing transforms live on the
Table (repository) where the honest return type can be a concrete array.
This also sidesteps the overwrite-formatter stacking limit of reshape(),
since the terminal methods consume a query and return an array rather than
mutating a chainable formatter.

- Add Table::projectAs(class-string, ?SelectQuery): list of DTOs. Delegates
  to the existing query projection path, so rows are mapped from the raw
  selected columns (entity visibility and accessors do not affect the data).
- Add Table::list(?SelectQuery, ...): terminal counterpart to the list
  finder, returning the combined array directly.
- Remove SelectQuery::reshape() (never released).
- Soft-deprecate SelectQuery::projectAs() in favor of Table::projectAs();
  doc-only for now since it is still used by createFromArray-style DTOs.
@dereuromark

dereuromark commented Jun 14, 2026

Copy link
Copy Markdown
Member Author

@dereuromark dereuromark changed the title Add SelectQuery::reshape() for type-tracked result reshaping Move result projection onto Table (Table::projectAs/list); drop SelectQuery::reshape() Jun 14, 2026
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.

3 participants