Skip to content

Commit 5b27169

Browse files
committed
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
1 parent 4ef26a3 commit 5b27169

2 files changed

Lines changed: 135 additions & 0 deletions

File tree

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

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

181188
Pagination
182189
----------

en/orm/query-builder.rst

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,134 @@ 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 are useful when
663+
you need a memory-efficient, read-only representation of your data, or when you
664+
want to decouple your data layer from the ORM's Entity objects.
665+
666+
The ``projectAs()`` method allows you to specify a DTO class that results will
667+
be hydrated into::
668+
669+
// Define a DTO class
670+
readonly class ArticleDto
671+
{
672+
public function __construct(
673+
public int $id,
674+
public string $title,
675+
public ?string $body = null,
676+
) {
677+
}
678+
}
679+
680+
// Use projectAs() to hydrate results into DTOs
681+
$articles = $articlesTable->find()
682+
->select(['id', 'title', 'body'])
683+
->projectAs(ArticleDto::class)
684+
->toArray();
685+
686+
DTOs typically consume about 3x less memory than Entity objects, making them
687+
ideal for read-heavy operations or when processing large result sets.
688+
689+
DTO Creation Methods
690+
^^^^^^^^^^^^^^^^^^^^
691+
692+
CakePHP supports two approaches for creating DTOs:
693+
694+
**Reflection-based constructor mapping** - CakePHP will use reflection to map
695+
database columns to constructor parameters::
696+
697+
readonly class ArticleDto
698+
{
699+
public function __construct(
700+
public int $id,
701+
public string $title,
702+
public ?AuthorDto $author = null,
703+
) {
704+
}
705+
}
706+
707+
**Factory method pattern** - If your DTO class has a ``createFromArray()``
708+
static method, CakePHP will use that instead::
709+
710+
class ArticleDto
711+
{
712+
public int $id;
713+
public string $title;
714+
715+
public static function createFromArray(
716+
array $data,
717+
bool $ignoreMissing = false
718+
): self {
719+
$dto = new self();
720+
$dto->id = $data['id'];
721+
$dto->title = $data['title'];
722+
723+
return $dto;
724+
}
725+
}
726+
727+
The factory method approach is approximately 2.5x faster than reflection-based
728+
hydration.
729+
730+
Nested Association DTOs
731+
^^^^^^^^^^^^^^^^^^^^^^^
732+
733+
You can project associated data into nested DTOs. Use the ``#[CollectionOf]``
734+
attribute to specify the type of elements in array properties::
735+
736+
use Cake\ORM\Attribute\CollectionOf;
737+
738+
readonly class ArticleDto
739+
{
740+
public function __construct(
741+
public int $id,
742+
public string $title,
743+
public ?AuthorDto $author = null,
744+
#[CollectionOf(CommentDto::class)]
745+
public array $comments = [],
746+
) {
747+
}
748+
}
749+
750+
readonly class AuthorDto
751+
{
752+
public function __construct(
753+
public int $id,
754+
public string $name,
755+
) {
756+
}
757+
}
758+
759+
readonly class CommentDto
760+
{
761+
public function __construct(
762+
public int $id,
763+
public string $body,
764+
) {
765+
}
766+
}
767+
768+
// Fetch articles with associations projected into DTOs
769+
$articles = $articlesTable->find()
770+
->contain(['Authors', 'Comments'])
771+
->projectAs(ArticleDto::class)
772+
->toArray();
773+
774+
.. note::
775+
776+
DTO projection is applied as the final formatting step, after all other
777+
formatters and behaviors have processed the results. This ensures
778+
compatibility with existing behavior formatters while still providing the
779+
benefits of DTOs.
780+
781+
.. versionadded:: 5.3.0
782+
The ``projectAs()`` method and ``#[CollectionOf]`` attribute were added.
783+
656784
.. _format-results:
657785

658786
Adding Calculated Fields

0 commit comments

Comments
 (0)