'Returning a child of class that is returned from a Trait

I then have a Base DTO class

class BaseDto
{
    public function __construct(array $dtoValues)
    {
        $this->properties = array_map(static function (ReflectionProperty $q) {
            return trim($q->name);
        }, (new ReflectionClass(get_class($this)))->getProperties(ReflectionProperty::IS_PUBLIC));

        foreach ($dtoValues as $propertyName => $value) {
            $propertyName = Str::camel($propertyName);
            if (in_array($propertyName, $this->properties, false)) {
                $this->{$propertyName} = $value;
            }
        }
    }
}

I also have an actual DTO class

class ModelDTO extends BaseDto
{
    public int $id
    public string $name; 
}

I have the following Trait in PHP

trait ToDtoTrait
{
    /**
     * @param string $dtoClassName
     * @return BaseDto
     * @throws InvalidArgumentException
     */
    public function toDto(string $dtoClassName): BaseDto;
    {
        $this->validateDtoClass($dtoClassName, BaseDto::class);

        return new $dtoClassName($this->toArray());
    }

    /**
     * @param string $dtoClassName
     * @param string $baseClassName
     * @return void
     */
    private function validateDtoClass(string $dtoClassName, string $baseClassName)
    {
        if (!class_exists($dtoClassName)) {
            throw new InvalidArgumentException("Trying to create a DTO for a class that doesn't exist: {$dtoClassName}");
        }

        if (!is_subclass_of($dtoClassName, $baseClassName)) {
            throw new InvalidArgumentException("Can not convert current object to '{$dtoClassName}' as it is not a valid DTO class: " . self::class);
        }
    }
}

That trait is then used inside of my Model classes

class MyDbModel
{
    use ToDtoTrait;
}

So this allows me to get an entry from the DB via the Model and then call toDto to receive an instance of the DTO class. Simple enough.

Now I have a service and this service will basically find the entry and return the DTO.

class MyService
{
    public function find(int $id): ?ModelDTO
    {
        $model = MyModel::find($id);
        if (empty($model)) {
            return null;
        }
        return $model->toDto();
    }
}

When I do it this way, I get a warning in the IDE:

Return value is expected to be '\App\Dtos\ModelDto|null', '\App\Dtos\BaseDto' returned 

How do I declare this so that people can see that MyService::find() returns an instance of ModelDto so they will have autocomplete for the attributes of the DTO and any other base functions that come with it (not shown here).



Solution 1:[1]

The warning is raised because the return type of ToDtoTrait::toDto isBaseDto while the return type of MyService::find is ?ModelDTO, which are polymorphically incompatible (a BaseDto is not necessarily a ModelDTO).

An easy solution is to narrow down the DTO type using instanceof:

// MyService::find
$model = MyModel::find($id);
if (empty($model)) {
    return null;
}
$dto = $model->toDto();
if (!$dto instanceof ModelDTO) {
    return null;
}
return $dto;

Sidenote: Why is ToDtoTrait::toDto called without arguments in MyService (return $model->toDto();)? Looks like you want to pass ModelDTO::class to it: return $model->toDto(ModelDTO::class);.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Jeroen van der Laan