3v4l.org

run code in 300+ PHP versions simultaneously
<?php declare(strict_types=1); /** * Shared by all CRUD DTO classes. Understood by service responsible for validation and persistence: CrudEntityManager. */ interface CrudDto { public function apply(): CrudEntity; public static function belongsTo(): string; } abstract class CrudEntity { // Shared stuff such as ID props etc, not important for the question at hand. } /** * Notice a few benefits:. * * 1. No setters * 2. The entity is in charge of updating itself from DTOs it declares it understands * 3. The entity can be created an updated using multiple different DTO objects, as long as the Entity has relevant methods to accept them * 4. No setters means guaranteed read-only properties where a value can be set only during the object creation are possible * 5. Using a static method instead of constructor with a lot of arguments makes inheritance easier */ class Person extends CrudEntity { protected string $name; protected string $writeOnceProperty; protected function __construct() { // Protecting the constructor forces usage of CreatePersonDto } public static function createWith(CreatePersonDto $dto): self { $self = new self(); $self->name = $dto->name; $self->writeOnceProperty = $dto->writeOnceProperty; return $self; } public function updateWith(UpdatePersonDto $dto): self { $this->name = $dto->name; return $this; } public function getName(): string { return $this->name; } public function getWriteOnceProperty(): string { return $this->writeOnceProperty; } } abstract class PersonDto implements CrudDto { public static function belongsTo(): string { return Person::class; } } class CreatePersonDto extends PersonDto { public function __construct(public string $name, public string $writeOnceProperty) { // Void } public function apply(): Person { return Person::createWith($this); } } class UpdatePersonDto extends PersonDto { public string $name; public function __construct(private readonly Person $source) { $this->name = $source->getName(); } public function apply(): Person { return $this->source->updateWith($this); } } class CrudManager { /** * IRL this would validate the DTO and store the $entity in the database. */ public function save(CrudDto $dto): CrudEntity { $entity = $dto->apply(); assert(is_a($entity, $dto::belongsTo())); return $entity; } /** * @template T of CrudEntity * * @param class-string<T> $className * * @return T */ public function saveWithTypeCheck(CrudDto $dto, string $className = CrudEntity::class): CrudEntity { $entity = $this->save($dto); assert($entity instanceof $className); return $entity; } } $manager = new CrudManager(); $dto = new CreatePersonDto(name: 'John Doe', writeOnceProperty: 'Born on 1/1/1970'); $person1 = $manager->save($dto); echo $person1->getName(); echo "\n"; echo get_debug_type($person1); // Prints Person, but static tools such as PHPStan or PHPStorm say it's a CrudEntity echo "\n\n\n"; // We can even use one DTO to create multiple different objects. Almost a prototype pattern, aye?! $person2 = $manager->saveWithTypeCheck($dto, Person::class); // Demo of the update action, just to prove the point $updateDto = new UpdatePersonDto($person2); $updateDto->name = 'New John'; $manager->save($updateDto); echo $person2->getName(); // New John echo "\n"; echo get_debug_type($person2); // Prints Person, but this time PHPStan and PHPStorm know it's CrudEntity|Person. // Conclusion: // // Using $person2 allows my IDE to do autocompletion, and PHPStan check types. // Using $person1 does none of that. // // Question: // // Is there any way to tell CrudManager to get the class-string<T of CrudEntity> from CrudDto::belongsTo()? // It would get rid of the need to pass the class name manually, as demonstrated in CrudManager::saveWithTypeCheck().

preferences:
36.47 ms | 406 KiB | 5 Q