proget-hq/phpstan-yii2

Problem with relations

b1rdex opened this issue ยท 7 comments

Code in the controller:

        /**
         * @var Transfer|null $transfer
         */
        $transfer = Transfer::find()->one();
        if (!$transfer) {
            return;
        }
        $paymentsTransfers = $transfer->getPaymentTransfer()->all();

Model:

class Transfer extends \yii\db\ActiveRecord
{
    /**
     * @return \yii\db\ActiveQuery
     */
    public function getPaymentTransfer()
    {
        return $this->hasMany(PaymentTransfer::class, ['transfer_id' => 'id']);
    }
}

getPaymentTransfer() method return type provided in PhpDoc or using native return type set to ActiveQuery leads to extension exception: Internal error: Unexpected type PHPStan\Type\ObjectType during method call all at line 142 (line 142 is $transfer->getPaymentTransfer()->all() call). That error comes from \Proget\PHPStan\Yii2\Type\ActiveQueryDynamicMethodReturnTypeExtension::getTypeFromMethodCall. Here is what's in the $calledOnType:

object(PHPStan\Type\ObjectType)#9382 (3) {
  ["className":"PHPStan\Type\ObjectType":private]=>
  string(18) "yii\db\ActiveQuery"
  ["subtractedType":"PHPStan\Type\ObjectType":private]=>
  NULL
  ["genericObjectType":"PHPStan\Type\ObjectType":private]=>
  NULL
}

If I remove return type from getPaymentTransfer() everything works fine. But that looks odd and that's easy to break if someone adds return type.

This is not limited to relations. I am facing this issue with simple methods returning yii\db\ActiveQuery, such as:

public static function findBySomething(string $something): \yii\db\ActiveQuery
{
    return self::find()
        ->where(['something' => $something]);
}

I've currently got a working version for

MyModel::findBySql("")->all();

and anything that returns an ActiveQuery.

However, saving it to a variable is still a work-in-progress (because I do not know how to access the MyModel part here):

$query = MyModel::findBySql("");
$query->all();

If anyone has any knowledge, there's an open question at phpstan.


Modified code - click to expand
public function getTypeFromMethodCall(MethodReflection $methodReflection, MethodCall $methodCall, Scope $scope): Type
    {
        $methodName = $methodReflection->getName();
        $calledOnType = $scope->getType($methodCall->var);
        
        if (!$calledOnType instanceof ActiveQueryObjectType) {
            if (!($calledOnType instanceof ObjectType) || $calledOnType->getClassName() !== ActiveQuery::class) {
                throw new ShouldNotHappenException(sprintf('Unexpected type %s during method call %s at line %d', \get_class($calledOnType), $methodReflection->getName(), $methodCall->getLine()));
            }

            $var = $methodCall->var;
            if ($var instanceof StaticCall) {
                /**
                 * @example
                 * MyModel::findBySql("")->all();
                 */
                $calledOnType = new ActiveQueryObjectType($var->class->toString(), $methodName !== 'one');
            } else if ($var instanceof Variable) {
                /**
                 * @example :
                 * $query = MyModel::findBySql("");
                 * $query->all();
                 */
                // TODO $calledOnType = new  ActiveQueryObjectType('??????????', $methodName === 'one');
            } else {
                throw new ShouldNotHappenException(sprintf('Unable to find ActiveRecord type for %s during method call %s at line %d', \get_class($calledOnType), $methodReflection->getName(), $methodCall->getLine()));
            }
        }
        
        if ($methodName === 'asArray') {
            $argType = isset($methodCall->args[0]) ? $scope->getType($methodCall->args[0]->value) : new ConstantBooleanType(true);
            if (!$argType instanceof ConstantBooleanType) {
                throw new ShouldNotHappenException(sprintf('Invalid argument provided to asArray method at line %d', $methodCall->getLine()));
            }

            return new ActiveQueryObjectType($calledOnType->getModelClass(), $argType->getValue());
        }

        if (!\in_array($methodName, ['one', 'all'], true)) {
            return new ActiveQueryObjectType($calledOnType->getModelClass(), $calledOnType->isAsArray());
        }

        if ($methodName === 'one') {
            return TypeCombinator::union(
                new NullType(),
                $calledOnType->isAsArray() ? new ArrayType(new StringType(), new MixedType()) : new ObjectType($calledOnType->getModelClass())
            );
        }

        return new ArrayType(
            new IntegerType(),
            $calledOnType->isAsArray() ? new ArrayType(new StringType(), new MixedType()) : new ObjectType($calledOnType->getModelClass())
        );
    }

I solved like this

/** @var null $query */
$query = Transfer::find()
/** @var Transfer $transfer */
$transfer = $query->one(); // @phpstan-ignore-line

i fix this #48

@marmichalski Hey! Are you repeating this error? A simple example that shows the current problem and is very annoying:

<?php

namespace myNameSpace;

use yii\db\ActiveQuery;

class MyQuery extends ActiveQuery
{
    public function test(array $ids): ActiveQuery
    {
        return $this->andWhere(['not in', 'id', $ids]);
    }
}
 ------ ---------------------------------------------------------------------------------------------- 
  Line   MyQuery.php                                                                      
 ------ ---------------------------------------------------------------------------------------------- 
         Internal error: Unexpected type PHPStan\Type\ThisType during method call andWhere at line 19  

I tried changing the validation myself, but my current knowledge is a bit lacking. I will try to figure it out further, but maybe you have a solution to this problem?

I am trying this out and also getting this error a lot:

Internal error: Unexpected type PHPStan\Type\ThisType during method call andWhere...

Anybody have a solution?

I solved like this

/** @var null $query */
$query = Transfer::find()
/** @var Transfer $transfer */
$transfer = $query->one(); // @phpstan-ignore-line

No, you did not solve, you put the issue under the carpet.