Data transfer objects (DTO) in Extbase
A data transfer object (DTO) is a special kind of model used to transport data between the business logic and the (Fluid) view but is not persisted to the database.
Data objects can be validated and passed as parameters to controller actions, just like persistable models.
A data object is defined by its values and cannot be referenced by an ID.
Forms are commonly bound to data objects. In multi-step forms, you can use multiple data objects with distinct validation rules for each step.
See also
Example: A BMI calculator without storage
The body mass index (BMI) calculator in this example contains a form where users can enter their height (in meters) and weight, and then calculate their BMI. The measurements need to be validated but do not have to be stored in the database.
We define an object to contain and transport the measurements required for our calculation:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Domain\Model\Dto;
final class MeasurementsDto
{
private float $height;
private int $weight;
public function __construct(?float $height = 0, ?int $weight = 0)
{
$this->height = $height ?? 0;
$this->weight = $weight ?? 0;
}
// getters and setters
}
If a constructor is defined, it will be called automatically; otherwise, the setter methods will be invoked.
See also
You can find the complete example on GitHub: https://github.com/TYPO3-Documentation/bmi_calculator
Using a DTO in the controller
You can use the DTO to pass default data to the form and to receive the form input:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Controller;
use Psr\Http\Message\ResponseInterface;
use T3docs\BmiCalculator\Domain\Model\Dto\MeasurementsDto;
use T3docs\BmiCalculator\Service\BmiCalculatorService;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class CalculatorController extends ActionController
{
public function __construct(
private readonly BmiCalculatorService $bmiCalculatorService,
) {}
public function formAction(): ResponseInterface
{
$defaultValues = new MeasurementsDto(1.73, 73);
$this->view->assign('defaultValues', $defaultValues);
return $this->htmlResponse();
}
public function resultAction(MeasurementsDto $measurements): ResponseInterface
{
$this->view->assign('measurements', $measurements);
$this->view->assign(
'result',
$this->bmiCalculatorService->calculate($measurements),
);
return $this->htmlResponse();
}
}
The DTO can also be used to transfer data to and from the business logic.
DTO and validation
All Extbase validators can be applied to a DTO as well as to an entity model.
The same # attribute can be used:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Domain\Model\Dto;
use TYPO3\CMS\Extbase\Attribute\Validate;
final class MeasurementsDto
{
// Ensure that the height is in meters, not centimeters
#[Validate(['validator' => 'NotEmpty'])]
#[Validate([
'validator' => 'NumberRange',
'options' => ['minimum' => 0.5, 'maximum' => 2.5],
])]
private float $height;
// Weight must not be empty
#[Validate(['validator' => 'NotEmpty'])]
private int $weight;
public function __construct(?float $height, ?int $weight)
{
$this->height = $height ?? 0;
$this->weight = $weight ?? 0;
}
// getters and setters
}
When an object is passed as an input parameter to a controller action that contains invalid data (for example, a height given in centimeters and therefore larger than 2.5), the Error action is called. If you have not overridden or changed the error action, it will display a flash message and attempt to return the user to the previous action.
You can override the default (and sometimes cryptic) error message by implementing the following method in your controller:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Controller;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class CalculatorController extends ActionController
{
// ...
protected function getErrorFlashMessage(): bool|string
{
return 'Check your measurements. ';
}
}
It is also possible to override the default error action error.
Converting DTOs to domain models
In order to be persisted to the database a DTO has to be transferred into a domain model.
This can be achieved by implementing a
static method converting the
DTO into a domain model:
To stay in our example above, let us assume we want to save the measurement input of our users into the database in order to use it for statistical purposes.
To avoid storing a float we will convert the height into centimeters.
As the static method is located in the same class it can access the protected properties of the model directly.
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Domain\Model;
use T3docs\BmiCalculator\Domain\Model\Dto\MeasurementsDto;
use TYPO3\CMS\Extbase\DomainObject\AbstractEntity;
final class Measurements extends AbstractEntity
{
protected int $height = 0;
protected int $weight = 0;
public static function fromMeasurementsDto(MeasurementsDto $measurementsDto): self
{
$model = new self();
$model->weight = $measurementsDto->getWeight();
$model->height = (int)($measurementsDto->getHeight() * 100);
return $model;
}
// Getters and Setters
}
You can now transfer your data object into a model entity that can be saved into the database:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Controller;
use Psr\Http\Message\ResponseInterface;
use T3docs\BmiCalculator\Domain\Model\Dto\MeasurementsDto;
use T3docs\BmiCalculator\Domain\Model\Measurements;
use T3docs\BmiCalculator\Domain\Repository\MeasurementsRepository;
use T3docs\BmiCalculator\Service\BmiCalculatorService;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class CalculatorController extends ActionController
{
public function __construct(
private readonly BmiCalculatorService $bmiCalculatorService,
private readonly MeasurementsRepository $measurementsRepository,
) {}
public function resultAction(MeasurementsDto $measurements): ResponseInterface
{
$this->view->assign('measurements', $measurements);
$this->view->assign(
'result',
$this->bmiCalculatorService->calculate($measurements),
);
$this->measurementsRepository->add(Measurements::fromMeasurementsDto($measurements));
return $this->htmlResponse();
}
// ...
}
Storing DTOs in the user session
DTOs are volatile by nature. The moment you leave the controller action they are gone.
Let us assume we want to remember the measurements of the user as long as they don't close their browser window.
We can now store the DTO into the user session (see Session data) even if no frontend user is logged in.
In order to store the DTO into the session we need to serialize it, turn it into a string. In order to load the DTO from the session we need to deserialize it, turn it from a string back into a DTO.
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Domain\Model\Dto;
final class MeasurementsDto
{
private float $height;
private int $weight;
public function __construct(?float $height = 0, ?int $weight = 0)
{
$this->height = $height ?? 0;
$this->weight = $weight ?? 0;
}
public function serialize(): string
{
return json_encode(
[
'height' => $this->height,
'weight' => $this->weight,
],
);
}
public static function deserialize(string $sessionData): self
{
$data = json_decode($sessionData);
if (!is_object($data) || !isset($data->height) || !isset($data->weight)) {
print_r($data);
throw new \RuntimeException('Deserialization failed ');
}
return new self(
height: (float)$data->height,
weight: (int)$data->weight,
);
}
// Getters and setters
}
We can now load the measurement from the session and store changes there. In order to have the functionality encapsulated we implement a service for it:
<?php
declare(strict_types=1);
namespace T3docs\BmiCalculator\Service;
use T3docs\BmiCalculator\Domain\Model\Dto\MeasurementsDto;
use TYPO3\CMS\Extbase\Mvc\RequestInterface;
use TYPO3\CMS\Frontend\Authentication\FrontendUserAuthentication;
class UserSessionService
{
public const SESSION_KEY = 'tx_bmi_calculator_session';
public function storeIntoSession(RequestInterface $request, MeasurementsDto $measurementsDto): void
{
$user = $this->getFrontendUser($request);
// We use type ses to store the data in the session
$user->setKey(
'ses',
self::SESSION_KEY,
$measurementsDto->serialize(),
);
// Important: store session data! Or it is not available in the next request!
$this->getFrontendUser($request)->storeSessionData();
}
public function getFromSession(RequestInterface $request): ?MeasurementsDto
{
$user = $this->getFrontendUser($request);
$data = $user->getKey('ses', self::SESSION_KEY);
if (!is_string($data)) {
return null;
}
return MeasurementsDto::deserialize($data);
}
private function getFrontendUser(RequestInterface $request): FrontendUserAuthentication
{
// This will create an anonymous frontend user if none is logged in
return $request->getAttribute('frontend.user');
}
}
We can then inject the Service into out controller and load and store the session data there:
<?php
declare(strict_types=1);
/*
* This file is part of the package t3docs/bmi-calculator.
*
* For the full copyright and license information, please read the
* LICENSE file that was distributed with this source code.
*/
namespace T3docs\BmiCalculator\Controller;
use Psr\Http\Message\ResponseInterface;
use T3docs\BmiCalculator\Domain\Model\Dto\MeasurementsDto;
use T3docs\BmiCalculator\Service\UserSessionService;
use TYPO3\CMS\Extbase\Mvc\Controller\ActionController;
class CalculatorController extends ActionController
{
public function __construct(
private readonly UserSessionService $userSessionService,
) {}
public function formAction(): ResponseInterface
{
$defaultValues = $this->userSessionService->loadFromSession($this->request)
?? new MeasurementsDto(1.73, 73);
$this->view->assign('defaultValues', $defaultValues);
return $this->htmlResponse();
}
public function resultAction(MeasurementsDto $measurements): ResponseInterface
{
$this->userSessionService->storeIntoSession($this->request, $measurements);
// ...
return $this->htmlResponse();
}
}
Using DTOs as demand objects
DTOs can also be used to handle instructional data. Let us assume we want to display the measurements of previous users in a table. This table can be sorted by weight or height and it can be filtered by different criteria.
We can use a DTO to transfer the current settings for the table and use this DTO for filtering and sorting in the repository.