I'm trying to remove some code duplication that has proven to be prone to human errors.
I created a working sample code at https://3v4l.org/QFA6m#v8.2.7 and a demo of PHPStan failing where expected, https://phpstan.org/r/e6310a8c-6691-4096-a698-44eadb1ab1f2
On a high level:
interface CrudDto
defines public function apply(): CrudEntity
class CreatePersonDto implements CrudDto
and its apply()
method returns a Person
class Person extends CrudEntity
CrudManager
class capable of working with my DTO through CrudManager::save(CrudDto $dto): CrudEntity
$crudManager->save($dto)
returns whatever the given DTO can create: PersonCreateDto
-> Person
, CatCreateDto
-> Cat
and so on, you get the point.I'm trying to tell CrudManager that it's not returning a CrudEntity
, but a Person
, or a Cat
, etc.
I achieved so by passing the expected class name to my save function and using things described here https://phpstan.org/blog/generics-by-examples#accept-a-class-string-and-return-the-object-of-that-type
But here comes the core of the question (thanks for reading until this point, btw!)
I would love to insulate the strict type checking so that the developer doesn't have to provide the expected type to the save()
method via a class-string<T of CrudEntity> $className
.
I want to do that because if the DTO knows what class it is creating, it should be able to give that information to whoever wants it.
I added CrudDto::belongsTo(): string
that returns the FQCN of whatever the entity the DTO can produce, and I can use that value to enforce the actual type on runtime, but I didn't find a way to inform static code analysers that the class-string is provided by the DTO itself, not by the argument next to it.
Effectively I'm looking for a pseudocode of something like this:
/**
* @template T of CrudEntity
* @param class-string<T> $dto::belongsTo()
*/
public function save(CrudDto $dto): CrudEntity;
In other words, I'd like to tell PHPStan, PHPStorm, psalm and others, that if CreatePersonDto::belongsTo()
returns Person::class
, calling $entityManager->save($personDto)
returns Person
, not just CrudDto
.
Can it be done without passing the expected return value's class name as a method argument of the save()
function?
Yes, it's definitely possible. See https://phpstan.org/r/50d160ac-e66c-4079-a8b6-474d0614cf56
First, you need to make your CrudDto
interface generic:
/** @template T of CrudEntity */
interface CrudDto {
// ...
}
Then, for every DTO implementation specify /** @extends CrudDto<EntityClass> */
:
/** @implements CrudDto<Person> */
abstract class PersonDto implements CrudDto {
// ...
}
And then you can make the CrudManager::save()
method generic:
/**
* @template T of CrudEntity
* @param CrudDto<T> $dto
* @return T
*/
public function save(CrudDto $dto): CrudEntity {
// ...
}