From 5b27169bde0f942d020806725717d999d239b60e Mon Sep 17 00:00:00 2001 From: mscherer Date: Tue, 30 Dec 2025 05:23:49 +0100 Subject: [PATCH 1/2] Add docs for SelectQuery::projectAs() DTO projection Documents the new projectAs() method for projecting ORM results into DTOs instead of Entity objects, including the #[CollectionOf] attribute for nested associations. Refs cakephp/cakephp#19135 --- en/appendices/5-3-migration-guide.rst | 7 ++ en/orm/query-builder.rst | 128 ++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) diff --git a/en/appendices/5-3-migration-guide.rst b/en/appendices/5-3-migration-guide.rst index 085c0b198d..5ff39ba315 100644 --- a/en/appendices/5-3-migration-guide.rst +++ b/en/appendices/5-3-migration-guide.rst @@ -177,6 +177,13 @@ ORM validation to the fields listed in the ``fields`` option. - Added ``TableContainer`` that you can register in your ``Application::services()`` to add dependency injection for your Tables. +- Added ``SelectQuery::projectAs()`` for projecting query results into Data + Transfer Objects (DTOs) instead of Entity objects. DTOs provide a + memory-efficient alternative (approximately 3x less memory than entities) for + read-only data access. See :ref:`dto-projection`. +- Added the ``#[CollectionOf]`` attribute for declaring the element type of + array properties in DTOs. This enables proper hydration of nested + associations into DTOs. Pagination ---------- diff --git a/en/orm/query-builder.rst b/en/orm/query-builder.rst index b395a7a75b..004272729f 100644 --- a/en/orm/query-builder.rst +++ b/en/orm/query-builder.rst @@ -653,6 +653,134 @@ After executing those lines, your result should look similar to this:: ... ] +.. _dto-projection: + +Projecting Results Into DTOs +---------------------------- + +In addition to fetching results as Entity objects or arrays, you can project +query results directly into Data Transfer Objects (DTOs). DTOs are useful when +you need a memory-efficient, read-only representation of your data, or when you +want to decouple your data layer from the ORM's Entity objects. + +The ``projectAs()`` method allows you to specify a DTO class that results will +be hydrated into:: + + // Define a DTO class + readonly class ArticleDto + { + public function __construct( + public int $id, + public string $title, + public ?string $body = null, + ) { + } + } + + // Use projectAs() to hydrate results into DTOs + $articles = $articlesTable->find() + ->select(['id', 'title', 'body']) + ->projectAs(ArticleDto::class) + ->toArray(); + +DTOs typically consume about 3x less memory than Entity objects, making them +ideal for read-heavy operations or when processing large result sets. + +DTO Creation Methods +^^^^^^^^^^^^^^^^^^^^ + +CakePHP supports two approaches for creating DTOs: + +**Reflection-based constructor mapping** - CakePHP will use reflection to map +database columns to constructor parameters:: + + readonly class ArticleDto + { + public function __construct( + public int $id, + public string $title, + public ?AuthorDto $author = null, + ) { + } + } + +**Factory method pattern** - If your DTO class has a ``createFromArray()`` +static method, CakePHP will use that instead:: + + class ArticleDto + { + public int $id; + public string $title; + + public static function createFromArray( + array $data, + bool $ignoreMissing = false + ): self { + $dto = new self(); + $dto->id = $data['id']; + $dto->title = $data['title']; + + return $dto; + } + } + +The factory method approach is approximately 2.5x faster than reflection-based +hydration. + +Nested Association DTOs +^^^^^^^^^^^^^^^^^^^^^^^ + +You can project associated data into nested DTOs. Use the ``#[CollectionOf]`` +attribute to specify the type of elements in array properties:: + + use Cake\ORM\Attribute\CollectionOf; + + readonly class ArticleDto + { + public function __construct( + public int $id, + public string $title, + public ?AuthorDto $author = null, + #[CollectionOf(CommentDto::class)] + public array $comments = [], + ) { + } + } + + readonly class AuthorDto + { + public function __construct( + public int $id, + public string $name, + ) { + } + } + + readonly class CommentDto + { + public function __construct( + public int $id, + public string $body, + ) { + } + } + + // Fetch articles with associations projected into DTOs + $articles = $articlesTable->find() + ->contain(['Authors', 'Comments']) + ->projectAs(ArticleDto::class) + ->toArray(); + +.. note:: + + DTO projection is applied as the final formatting step, after all other + formatters and behaviors have processed the results. This ensures + compatibility with existing behavior formatters while still providing the + benefits of DTOs. + +.. versionadded:: 5.3.0 + The ``projectAs()`` method and ``#[CollectionOf]`` attribute were added. + .. _format-results: Adding Calculated Fields From 90807912bd2060cd150418414dee7210f6d60c3b Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 31 Dec 2025 12:20:55 +0100 Subject: [PATCH 2/2] Address PR feedback - Highlight advantages over arrays (type safety, IDE support, decoupled serialization) - Add API response example showing DTOs used outside ORM context --- en/orm/query-builder.rst | 63 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/en/orm/query-builder.rst b/en/orm/query-builder.rst index 004272729f..618a82be04 100644 --- a/en/orm/query-builder.rst +++ b/en/orm/query-builder.rst @@ -659,9 +659,18 @@ Projecting Results Into DTOs ---------------------------- In addition to fetching results as Entity objects or arrays, you can project -query results directly into Data Transfer Objects (DTOs). DTOs are useful when -you need a memory-efficient, read-only representation of your data, or when you -want to decouple your data layer from the ORM's Entity objects. +query results directly into Data Transfer Objects (DTOs). DTOs offer several +advantages: + +- **Memory efficiency** - DTOs consume approximately 3x less memory than Entity + objects, making them ideal for large result sets. +- **Type safety** - DTOs provide strong typing and IDE autocompletion support, + unlike plain arrays. +- **Decoupled serialization** - DTOs let you separate your API response + structure from your database schema, making it easier to version APIs or + expose only specific fields. +- **Read-only data** - Using ``readonly`` classes ensures data integrity and + makes your intent clear. The ``projectAs()`` method allows you to specify a DTO class that results will be hydrated into:: @@ -683,9 +692,6 @@ be hydrated into:: ->projectAs(ArticleDto::class) ->toArray(); -DTOs typically consume about 3x less memory than Entity objects, making them -ideal for read-heavy operations or when processing large result sets. - DTO Creation Methods ^^^^^^^^^^^^^^^^^^^^ @@ -771,6 +777,51 @@ attribute to specify the type of elements in array properties:: ->projectAs(ArticleDto::class) ->toArray(); +Using DTOs for API Responses +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +DTOs are particularly useful for building API responses where you want to +control the output structure independently from your database schema. You can +define a DTO that represents your API contract and include custom serialization +logic:: + + readonly class ArticleApiResponse + { + public function __construct( + public int $id, + public string $title, + public string $slug, + public string $authorName, + public string $publishedAt, + ) { + } + + public static function createFromArray( + array $data, + bool $ignoreMissing = false + ): self { + return new self( + id: $data['id'], + title: $data['title'], + slug: Inflector::slug($data['title']), + authorName: $data['author']['name'] ?? 'Unknown', + publishedAt: $data['created']->format('c'), + ); + } + } + + // In your controller + $articles = $this->Articles->find() + ->contain(['Authors']) + ->projectAs(ArticleApiResponse::class) + ->toArray(); + + return $this->response->withType('application/json') + ->withStringBody(json_encode(['articles' => $articles])); + +This approach keeps your API response format decoupled from your database +schema, making it easier to evolve your API without changing your data model. + .. note:: DTO projection is applied as the final formatting step, after all other