Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions en/appendices/5-3-migration-guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Comment on lines +181 to +183
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we also want to highlight the differences from arrays like typehinting, IDE support, and separating serialization logic/output from storage schema?

- Added the ``#[CollectionOf]`` attribute for declaring the element type of
array properties in DTOs. This enables proper hydration of nested
associations into DTOs.

Pagination
----------
Expand Down
179 changes: 179 additions & 0 deletions en/orm/query-builder.rst
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,185 @@ 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 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::

// 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();

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();

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
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
Expand Down
Loading