Skip to content

Idea: Unify SELECT / DML / hydration under PostQueryInterface #88

@koriym

Description

@koriym

Background

#85 introduced PostQueryInterface / PostQueryContext for DML results. While reviewing, it became clear that the same mechanism could generalise beyond DML — SELECT results, collection wrapping, and entity composition could all ride on the same dispatch. This issue captures that direction as a memo, not committed work.

The insight

Today DbQueryInterceptor has two paths:

  1. Return type implements PostQueryInterface → call execPostQuery → delegate to the class's static factory.
  2. Otherwise → classic path: fetch rows, hydrate into entity / array, return.

If any return type that implements PostQueryInterface — including SELECT-facing wrapper classes — goes through path 1, we end up with a single extension mechanism. The implementing class owns its own construction; the framework doesn't need to branch.

Shape of the unified design

The implementing class decides what it is — single entity, collection wrapper, or DML result. Each form looks the same from the framework's side: one static factory, one context.

// DML: rowcount for UPDATE / DELETE
final class AffectedRows implements PostQueryInterface { /* uses \$context->statement->rowCount() */ }

// DML: resolved params + insert id for INSERT
final class InsertedRow implements PostQueryInterface { /* uses \$context->values, \$context->pdo->lastInsertId() */ }

// SELECT: collection wrapper around hydrated entities
final class Articles implements PostQueryInterface
{
    public function __construct(public readonly Collection \$collection) {}

    public static function postQuery(PostQueryContext \$context): static
    {
        return new static(new Collection(\$context->rows));  // rows already hydrated as array<Article>
    }
}

// SELECT: entity that also wants framework metadata
final class Article implements PostQueryInterface
{
    // ...fields plus meta like \$sourceSql, \$boundValues, etc.
    public static function postQuery(PostQueryContext \$context): static { ... }
}

Crucial detail: the framework owns hydration, not the factory

A naive design would hand the factory the raw PDOStatement and expect it to fetchAll(PDO::FETCH_ASSOC) itself. That breaks down fast:

  • DI-backed entity hydration (BEAR-style resources, constructor injection, factories resolved via Injector) needs the FetchInterface / FetchFactory pipeline and the container. Pushing that into PostQueryInterface implementations drags Injector into the context and forces every result class to reimplement hydration.
  • Different fetch modes (FETCH_ASSOC vs object vs factory-driven) are already handled by the existing pipeline.

The clean split is:

  • Framework / interceptor / perform() — run the existing FetchInterface + FetchFactory + Injector pipeline and produce array<Article> (or whatever hydrated form the pipeline supports today).
  • PostQueryInterface implementation — receives the hydrated result via the context and composes it (wraps in a Collection, adds metadata, selects a single element, etc.). It never touches raw rows or DI.

So PostQueryContext grows one field:

final class PostQueryContext
{
    public function __construct(
        public readonly PDOStatement \$statement,
        public readonly ExtendedPdoInterface \$pdo,
        public readonly array \$values,
        public readonly array \$rows = [],  // hydrated entities for SELECT paths, empty for DML
    ) {}
}

What about static return type?

postQuery(): static means a class can only return itself. An Article cannot return new LaravelCollection(...) from its factory — type mismatch.

The correct pattern is composition, not inheritance or substitution: write a dedicated class (Articles) that implements PostQueryInterface and holds a Collection as a property.

\$articles = \$repo->list();  // returns Articles
\$articles->collection->map(...)->filter(...);  // access the underlying Collection

Benefits over extends Collection:

  • Free choice of Collection library (Laravel / illuminate / doctrine / custom).
  • Other metadata can live alongside on the same class (\$articles->executedSql, \$articles->totalCount, etc.).
  • No multiple-inheritance tension.

Disposition

Still speculative — the current SELECT side (FetchInterface + entity hydration) covers real use cases already. Defer until a concrete request for SELECT-side composition or metadata surfaces. When it does, the existing PostQueryInterface + PostQueryContext vocabulary is already the right shape; the change is small:

  • Extend PostQueryContext with \$rows (hydrated entities).
  • Extend DbQueryInterceptor so SELECT-side return types implementing PostQueryInterface route through the same path, with hydration done before the factory call.
  • No new interface, no FetchContext sibling — same vocabulary covers both axes.

Refs #85

Metadata

Metadata

Assignees

No one assigned

    Labels

    ideaDesign ideas / future considerations — not committed work

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions