proget-hq / phpstan-yii2

Yii2 extension for PHPStan
MIT License
52 stars 18 forks source link

Problem with relations #25

Open b1rdex opened 4 years ago

b1rdex commented 4 years ago

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.

flaviovs commented 4 years ago

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]);
}
EtienneBruines commented 3 years ago

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 ```php 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()) ); } ```
XMarat commented 2 years ago

I solved like this

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

i fix this https://github.com/proget-hq/phpstan-yii2/pull/48

porozhnyy commented 2 years ago

@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?

optmsp commented 2 years ago

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?

lisotton commented 1 year ago

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.