More about PHP readonly modifier
The readonly
modifier in PHP is intended for declaring class properties whose values cannot be changed after they are initialized in the constructor.
class UserDTO
{
public function __construct(
public readonly string $name,
public readonly int $age,
) {
}
}
If every class property is declared as readonly
, your IDE can advise you to move that modifier on a class level.
This is really ideal for classes that represent immutable data - such as Data Transfer Objects (DTO), Value Objects (VO) or domain models, where the integrity and immutability of data after object creation is important.
readonly class UserDTO
{
public function __construct(
public string $name,
public int $age,
) {
}
}
In this example, the UserDTO is an immutable object, because of using readonly
modifier on a class level, making code easier to develop and maintain.
On the other hand, applying readonly
to whole service classes that include dependencies on other services (via Dependency Injection, DI) can complicate writing unit tests.
In services where dependencies need to be replaceable (mockable) in tests, readonly
can be a hindrance:
readonly class UserValidator
{
public function __construct(
public UserService $userService,
) {
}
public function checkExistence(int $id): bool
{
return $this->userService->getUser($id) !== null;
}
}
readonly class UserService
{
public function __construct(
public UserRepository $userRepository,
) {
}
public function getUser(int $id): ?User
{
return $this->userRepository->findById($id);
}
}
Here UserValidator
depends on UserService
, which itself depends on UserRepository
and if the whole UserValidator
and UserService
service-classes are declared as readonly
, they cannot be replaced with mock in tests (for example, if we are testing UserValidator
and want to mock UserService
itself).
This limits flexibility in testing, especially when you need to isolate a class to test its functionality away from its dependencies.
PHPUnit\Framework\MockObject\ClassIsReadonlyException: Class "UserService" is declared "readonly" and cannot be doubled
Use readonly
modifier on a class level for data-based objects, not services: limit the use of readonly
to classes that model data and do not contain business logic or complex dependencies. This keeps the design clean and makes testing easier.
class UserValidator
{
public function __construct(
public readonly UserService $userService,
) {
}
public function checkExistence(int $id): bool
{
return $this->userService->getUser($id) !== null;
}
}
class UserService
{
public function __construct(
public readonly UserRepository $userRepository,
) {
}
public function getUser(int $id): ?User
{
return $this->userRepository->findById($id);
}
}
Separate business logic from state: Consider separating classes into those that manage state (and can use readonly
) and those that contain business logic and depend on DI.