@@ -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
658837Adding Calculated Fields
0 commit comments