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:
- I am using DTO capable of creating an object of another class
interface CrudDtodefinespublic function apply(): CrudEntityclass CreatePersonDto implements CrudDtoand itsapply()method returns aPersonclass Person extends CrudEntity
- There is a
CrudManagerclass capable of working with my DTO throughCrudManager::save(CrudDto $dto): CrudEntity - Calling
$crudManager->save($dto)returns whatever the given DTO can create:PersonCreateDto->Person,CatCreateDto->Catand 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
CrudDtointerface generic:Then, for every DTO implementation specify
/** @extends CrudDto<EntityClass> */:And then you can make the
CrudManager::save()method generic: