diff --git a/src/Type/ActiveRecordFindReturnTypeExtension.php b/src/Type/ActiveRecordFindReturnTypeExtension.php index 080b4e4..4e065af 100644 --- a/src/Type/ActiveRecordFindReturnTypeExtension.php +++ b/src/Type/ActiveRecordFindReturnTypeExtension.php @@ -15,6 +15,7 @@ use PHPStan\Type\NeverType; use PHPStan\Type\Type; use PHPStan\Type\TypeCombinator; +use yii\db\ActiveRecord; use yii\db\ActiveRecordInterface; /** @@ -34,13 +35,27 @@ public function getClass(): string { } public function isStaticMethodSupported(MethodReflection $methodReflection): bool { - return $methodReflection->getName() === 'find'; + return in_array($methodReflection->getName(), ['find', 'findBySql', 'findByCondition'], true); } - public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, StaticCall $methodCall, Scope $scope): ?Type { + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): ?Type { $calledOn = $methodCall->class; + $declaringClass = $methodReflection->getDeclaringClass(); + // The implementations of the ::findBySql() and ::findByCondition() methods rely on the ::find() method, + // so unless they have been overridden, we return the ::find() method type + if ($methodReflection->getName() !== 'find' && $declaringClass->getName() === ActiveRecord::class) { + $findMethod = $declaringClass->getMethod('find', $scope); + $findCall = new StaticCall($calledOn, 'find'); // According to the Yii2 implementation, this call will have no arguments + + return $this->getTypeFromStaticMethodCall($findMethod, $findCall, $scope); + } + if ($calledOn instanceof Name) { - return $this->createType($scope->resolveName($calledOn), $scope); + return $this->createType($scope->resolveName($calledOn), $methodReflection->getName(), $scope); } $types = []; @@ -50,7 +65,7 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return new NeverType(); } - $types[] = $this->createType($constantString->getValue(), $scope); + $types[] = $this->createType($constantString->getValue(), $methodReflection->getName(), $scope); } return TypeCombinator::union(...$types); @@ -59,8 +74,8 @@ public function getTypeFromStaticMethodCall(MethodReflection $methodReflection, return null; } - private function createType(string $modelClass, Scope $scope): Type { - $method = $this->reflectionProvider->getClass($modelClass)->getMethod('find', $scope); + private function createType(string $modelClass, string $methodName, Scope $scope): Type { + $method = $this->reflectionProvider->getClass($modelClass)->getMethod($methodName, $scope); $returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); if (!$returnType->isObject()->yes()) { throw new ShouldNotHappenException(); diff --git a/tests/Type/_data/active-query-builder-return-type.php b/tests/Type/_data/active-query-builder-return-type.php index 00dcb15..d1137df 100644 --- a/tests/Type/_data/active-query-builder-return-type.php +++ b/tests/Type/_data/active-query-builder-return-type.php @@ -10,18 +10,30 @@ assertType(Article::class . '|null', Article::find()->one()); assertType('array', Article::find()->all()); +assertType(Article::class . '|null', Article::findBySql('')->one()); +assertType('array', Article::findBySql('')->all()); + // Preserve when built-in filtering assertType('array', Comment::find()->andWhere(['user_id' => 123])->all()); +assertType('array', Comment::findBySql('')->andWhere(['user_id' => 123])->all()); + // Preserve when custom filter assertType('array', Comment::find()->notDeletedSelf()->all()); assertType('array', Comment::find()->notDeletedStatic()->all()); assertType('array', Comment::find()->notDeletedThis()->all()); +assertType('array', Comment::findBySql('')->notDeletedSelf()->all()); +assertType('array', Comment::findBySql('')->notDeletedStatic()->all()); +assertType('array', Comment::findBySql('')->notDeletedThis()->all()); + // As array assertType('array|null', Comment::find()->asArray()->one()); assertType('array>', Comment::find()->asArray()->all()); +assertType('array|null', Comment::findBySql('')->asArray()->one()); +assertType('array>', Comment::findBySql('')->asArray()->all()); + // Index by assertType('array', Comment::find()->indexBy('user_id')->all()); assertType('array', Comment::find()->indexBy(fn() => 'key')->all()); @@ -30,14 +42,31 @@ assertType('array', Comment::find()->indexBy(null)->all()); assertType('array>', Comment::find()->asArray()->indexBy(null)->all()); +assertType('array', Comment::findBySql('')->indexBy('user_id')->all()); +assertType('array', Comment::findBySql('')->indexBy(fn() => 'key')->all()); +assertType('array>', Comment::findBySql('')->asArray()->indexBy('user_id')->all()); +assertType('array>', Comment::findBySql('')->asArray()->indexBy(fn() => 'key')->all()); +assertType('array', Comment::findBySql('')->indexBy(null)->all()); +assertType('array>', Comment::findBySql('')->asArray()->indexBy(null)->all()); + // Batch assertType(BatchQueryResult::class . '>', Comment::find()->batch(250)); assertType(BatchQueryResult::class . '>>', Comment::find()->asArray()->batch(250)); assertType(BatchQueryResult::class . '>', Comment::find()->indexBy('user_id')->batch(250)); assertType(BatchQueryResult::class . '>>', Comment::find()->asArray()->indexBy('user_id')->batch(250)); +assertType(BatchQueryResult::class . '>', Comment::findBySql('')->batch(250)); +assertType(BatchQueryResult::class . '>>', Comment::findBySql('')->asArray()->batch(250)); +assertType(BatchQueryResult::class . '>', Comment::findBySql('')->indexBy('user_id')->batch(250)); +assertType(BatchQueryResult::class . '>>', Comment::findBySql('')->asArray()->indexBy('user_id')->batch(250)); + // Each assertType(BatchQueryResult::class . '', Comment::find()->each(250)); assertType(BatchQueryResult::class . '>', Comment::find()->asArray()->each(250)); assertType(BatchQueryResult::class . '', Comment::find()->indexBy('user_id')->each(250)); assertType(BatchQueryResult::class . '>', Comment::find()->asArray()->indexBy('user_id')->each(250)); + +assertType(BatchQueryResult::class . '', Comment::findBySql('')->each(250)); +assertType(BatchQueryResult::class . '>', Comment::findBySql('')->asArray()->each(250)); +assertType(BatchQueryResult::class . '', Comment::findBySql('')->indexBy('user_id')->each(250)); +assertType(BatchQueryResult::class . '>', Comment::findBySql('')->asArray()->indexBy('user_id')->each(250)); diff --git a/tests/Type/_data/active-record-find-return-type.php b/tests/Type/_data/active-record-find-return-type.php index b0fba6f..f166136 100644 --- a/tests/Type/_data/active-record-find-return-type.php +++ b/tests/Type/_data/active-record-find-return-type.php @@ -8,13 +8,23 @@ use function PHPStan\Testing\assertType; assertType(ActiveQuery::class . '<' . Article::class . '>', Article::find()); +assertType(ActiveQuery::class . '<' . Article::class . '>', Article::findBySql('')); assertType(CommentsQuery::class . '<' . Comment::class . '>', Comment::find()); +assertType(CommentsQuery::class . '<' . Comment::class . '>', Comment::findBySql('')); $class = Article::class; assertType(ActiveQuery::class . '<' . Article::class . '>', $class::find()); +assertType(ActiveQuery::class . '<' . Article::class . '>', $class::findBySql('')); if (random_int(0, 10) === 0) { $class = Comment::class; } -assertType(CommentsQuery::class . '<' . Comment::class . '>|' . ActiveQuery::class . '<' . Article::class . '>', $class::find()); +assertType( + CommentsQuery::class . '<' . Comment::class . '>|' . ActiveQuery::class . '<' . Article::class . '>', + $class::find(), +); +assertType( + CommentsQuery::class . '<' . Comment::class . '>|' . ActiveQuery::class . '<' . Article::class . '>', + $class::findBySql(''), +); diff --git a/tests/Type/_data/active-record-object-type.php b/tests/Type/_data/active-record-object-type.php index eababb1..deb3324 100644 --- a/tests/Type/_data/active-record-object-type.php +++ b/tests/Type/_data/active-record-object-type.php @@ -8,10 +8,16 @@ assertType('bool', isset(Article::find()->one()['id'])); assertType('bool', isset(Article::find()->one()['text'])); +assertType('bool', isset(Article::findBySql('')->one()['id'])); +assertType('bool', isset(Article::findBySql('')->one()['text'])); + // Read assertType('int', Article::find()->one()['id']); assertType('string', Article::find()->one()['text']); +assertType('int', Article::findBySql('')->one()['id']); +assertType('string', Article::findBySql('')->one()['text']); + // Write $article = Article::find()->one(); $article['id'] = 123;