OrderRepository in a constructor. Click. I am there. That is how I explore a big Symfony project now, and I noticed my main reason for types has changed. Not bug catching anymore. Navigation.
Symfony 5.1 is out, PHP 7.4 is everywhere I work, and the IDE knows every caller and every implementation. Compare with the array-passing style we all wrote for years:
public function register($data)
{
// what is in $data? read three call sites to find out
}
versus
public function register(RegistrationRequest $request): RegistrationResult
The second signature is documentation that cannot rot. RegistrationRequest with typed properties shows the exact shape, and psalm or phpstan tells me at commit time when someone forgets a field, instead of an Undefined index in the logs at 3am. The runtime error and the static error carry the same information. One of them arrives before deploy.
Autowiring completes the picture. People call it magic. I think it is the opposite. The old magic was container config in YAML, strings pointing at strings. Now the constructor type is the wiring. The class says what it needs, in code, where I am already looking.
One warning from our own codebase. Once DTOs feel good, someone generates one per table, one per endpoint, and mappers between all of them. A new field means five files. A DTO should exist because some boundary needs that exact shape: HTTP input, a message payload, a report row. If it is a one-to-one copy of an entity and only carries data between two layers invented last month, delete the layer.
Type the boundaries. Keep the middle simple.
The someone with the mappers was me, two projects ago. It looked very clean in the pull request.