Skip to content

Commit 1d06379

Browse files
authored
Merge pull request #8148 from cakephp/docs-dto-projection
Add docs for SelectQuery::projectAs() DTO projection
2 parents eaa88f0 + 9080791 commit 1d06379

File tree

2 files changed

+186
-0
lines changed

2 files changed

+186
-0
lines changed

en/appendices/5-3-migration-guide.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,13 @@ ORM
186186
validation to the fields listed in the ``fields`` option.
187187
- Added ``TableContainer`` that you can register in your ``Application::services()`` to
188188
add dependency injection for your Tables.
189+
- Added ``SelectQuery::projectAs()`` for projecting query results into Data
190+
Transfer Objects (DTOs) instead of Entity objects. DTOs provide a
191+
memory-efficient alternative (approximately 3x less memory than entities) for
192+
read-only data access. See :ref:`dto-projection`.
193+
- Added the ``#[CollectionOf]`` attribute for declaring the element type of
194+
array properties in DTOs. This enables proper hydration of nested
195+
associations into DTOs.
189196

190197
Pagination
191198
----------

en/orm/query-builder.rst

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,185 @@ After executing those lines, your result should look similar to this::
653653
...
654654
]
655655

656+
.. _dto-projection:
657+
658+
Projecting Results Into DTOs
659+
----------------------------
660+
661+
In addition to fetching results as Entity objects or arrays, you can project
662+
query results directly into Data Transfer Objects (DTOs). DTOs offer several
663+
advantages:
664+
665+
- **Memory efficiency** - DTOs consume approximately 3x less memory than Entity
666+
objects, making them ideal for large result sets.
667+
- **Type safety** - DTOs provide strong typing and IDE autocompletion support,
668+
unlike plain arrays.
669+
- **Decoupled serialization** - DTOs let you separate your API response
670+
structure from your database schema, making it easier to version APIs or
671+
expose only specific fields.
672+
- **Read-only data** - Using ``readonly`` classes ensures data integrity and
673+
makes your intent clear.
674+
675+
The ``projectAs()`` method allows you to specify a DTO class that results will
676+
be hydrated into::
677+
678+
// Define a DTO class
679+
readonly class ArticleDto
680+
{
681+
public function __construct(
682+
public int $id,
683+
public string $title,
684+
public ?string $body = null,
685+
) {
686+
}
687+
}
688+
689+
// Use projectAs() to hydrate results into DTOs
690+
$articles = $articlesTable->find()
691+
->select(['id', 'title', 'body'])
692+
->projectAs(ArticleDto::class)
693+
->toArray();
694+
695+
DTO Creation Methods
696+
^^^^^^^^^^^^^^^^^^^^
697+
698+
CakePHP supports two approaches for creating DTOs:
699+
700+
**Reflection-based constructor mapping** - CakePHP will use reflection to map
701+
database columns to constructor parameters::
702+
703+
readonly class ArticleDto
704+
{
705+
public function __construct(
706+
public int $id,
707+
public string $title,
708+
public ?AuthorDto $author = null,
709+
) {
710+
}
711+
}
712+
713+
**Factory method pattern** - If your DTO class has a ``createFromArray()``
714+
static method, CakePHP will use that instead::
715+
716+
class ArticleDto
717+
{
718+
public int $id;
719+
public string $title;
720+
721+
public static function createFromArray(
722+
array $data,
723+
bool $ignoreMissing = false
724+
): self {
725+
$dto = new self();
726+
$dto->id = $data['id'];
727+
$dto->title = $data['title'];
728+
729+
return $dto;
730+
}
731+
}
732+
733+
The factory method approach is approximately 2.5x faster than reflection-based
734+
hydration.
735+
736+
Nested Association DTOs
737+
^^^^^^^^^^^^^^^^^^^^^^^
738+
739+
You can project associated data into nested DTOs. Use the ``#[CollectionOf]``
740+
attribute to specify the type of elements in array properties::
741+
742+
use Cake\ORM\Attribute\CollectionOf;
743+
744+
readonly class ArticleDto
745+
{
746+
public function __construct(
747+
public int $id,
748+
public string $title,
749+
public ?AuthorDto $author = null,
750+
#[CollectionOf(CommentDto::class)]
751+
public array $comments = [],
752+
) {
753+
}
754+
}
755+
756+
readonly class AuthorDto
757+
{
758+
public function __construct(
759+
public int $id,
760+
public string $name,
761+
) {
762+
}
763+
}
764+
765+
readonly class CommentDto
766+
{
767+
public function __construct(
768+
public int $id,
769+
public string $body,
770+
) {
771+
}
772+
}
773+
774+
// Fetch articles with associations projected into DTOs
775+
$articles = $articlesTable->find()
776+
->contain(['Authors', 'Comments'])
777+
->projectAs(ArticleDto::class)
778+
->toArray();
779+
780+
Using DTOs for API Responses
781+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
782+
783+
DTOs are particularly useful for building API responses where you want to
784+
control the output structure independently from your database schema. You can
785+
define a DTO that represents your API contract and include custom serialization
786+
logic::
787+
788+
readonly class ArticleApiResponse
789+
{
790+
public function __construct(
791+
public int $id,
792+
public string $title,
793+
public string $slug,
794+
public string $authorName,
795+
public string $publishedAt,
796+
) {
797+
}
798+
799+
public static function createFromArray(
800+
array $data,
801+
bool $ignoreMissing = false
802+
): self {
803+
return new self(
804+
id: $data['id'],
805+
title: $data['title'],
806+
slug: Inflector::slug($data['title']),
807+
authorName: $data['author']['name'] ?? 'Unknown',
808+
publishedAt: $data['created']->format('c'),
809+
);
810+
}
811+
}
812+
813+
// In your controller
814+
$articles = $this->Articles->find()
815+
->contain(['Authors'])
816+
->projectAs(ArticleApiResponse::class)
817+
->toArray();
818+
819+
return $this->response->withType('application/json')
820+
->withStringBody(json_encode(['articles' => $articles]));
821+
822+
This approach keeps your API response format decoupled from your database
823+
schema, making it easier to evolve your API without changing your data model.
824+
825+
.. note::
826+
827+
DTO projection is applied as the final formatting step, after all other
828+
formatters and behaviors have processed the results. This ensures
829+
compatibility with existing behavior formatters while still providing the
830+
benefits of DTOs.
831+
832+
.. versionadded:: 5.3.0
833+
The ``projectAs()`` method and ``#[CollectionOf]`` attribute were added.
834+
656835
.. _format-results:
657836

658837
Adding Calculated Fields

0 commit comments

Comments
 (0)