Skip to content

Commit

Permalink
Added ActiveRecord extension type to support ::findBySql() and ::find…
Browse files Browse the repository at this point in the history
…ByCondition() methods usages (#6)

* added ActiveRecord extension type to support findBySql method usages

* deleted user specific config from project .gitignore

* made findBySql inherit types from find

* Rework implementation

---------

Co-authored-by: ErickSkrauch <erickskrauch@yandex.ru>
  • Loading branch information
iVovolk and erickskrauch committed May 28, 2024
1 parent 7413f89 commit f841ad3
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 7 deletions.
27 changes: 21 additions & 6 deletions src/Type/ActiveRecordFindReturnTypeExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use yii\db\ActiveRecord;
use yii\db\ActiveRecordInterface;

/**
Expand All @@ -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 = [];
Expand All @@ -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);
Expand All @@ -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();
Expand Down
29 changes: 29 additions & 0 deletions tests/Type/_data/active-query-builder-return-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,30 @@
assertType(Article::class . '|null', Article::find()->one());
assertType('array<int, ' . Article::class . '>', Article::find()->all());

assertType(Article::class . '|null', Article::findBySql('')->one());
assertType('array<int, ' . Article::class . '>', Article::findBySql('')->all());

// Preserve when built-in filtering
assertType('array<int, ' . Comment::class . '>', Comment::find()->andWhere(['user_id' => 123])->all());

assertType('array<int, ' . Comment::class . '>', Comment::findBySql('')->andWhere(['user_id' => 123])->all());

// Preserve when custom filter
assertType('array<int, ' . Comment::class . '>', Comment::find()->notDeletedSelf()->all());
assertType('array<int, ' . Comment::class . '>', Comment::find()->notDeletedStatic()->all());
assertType('array<int, ' . Comment::class . '>', Comment::find()->notDeletedThis()->all());

assertType('array<int, ' . Comment::class . '>', Comment::findBySql('')->notDeletedSelf()->all());
assertType('array<int, ' . Comment::class . '>', Comment::findBySql('')->notDeletedStatic()->all());
assertType('array<int, ' . Comment::class . '>', Comment::findBySql('')->notDeletedThis()->all());

// As array
assertType('array<string, mixed>|null', Comment::find()->asArray()->one());
assertType('array<int, array<string, mixed>>', Comment::find()->asArray()->all());

assertType('array<string, mixed>|null', Comment::findBySql('')->asArray()->one());
assertType('array<int, array<string, mixed>>', Comment::findBySql('')->asArray()->all());

// Index by
assertType('array<string, ' . Comment::class . '>', Comment::find()->indexBy('user_id')->all());
assertType('array<string, ' . Comment::class . '>', Comment::find()->indexBy(fn() => 'key')->all());
Expand All @@ -30,14 +42,31 @@
assertType('array<int, ' . Comment::class . '>', Comment::find()->indexBy(null)->all());
assertType('array<int, array<string, mixed>>', Comment::find()->asArray()->indexBy(null)->all());

assertType('array<string, ' . Comment::class . '>', Comment::findBySql('')->indexBy('user_id')->all());
assertType('array<string, ' . Comment::class . '>', Comment::findBySql('')->indexBy(fn() => 'key')->all());
assertType('array<string, array<string, mixed>>', Comment::findBySql('')->asArray()->indexBy('user_id')->all());
assertType('array<string, array<string, mixed>>', Comment::findBySql('')->asArray()->indexBy(fn() => 'key')->all());
assertType('array<int, ' . Comment::class . '>', Comment::findBySql('')->indexBy(null)->all());
assertType('array<int, array<string, mixed>>', Comment::findBySql('')->asArray()->indexBy(null)->all());

// Batch
assertType(BatchQueryResult::class . '<int, array<int, ' . Comment::class . '>>', Comment::find()->batch(250));
assertType(BatchQueryResult::class . '<int, array<int, array<string, mixed>>>', Comment::find()->asArray()->batch(250));
assertType(BatchQueryResult::class . '<int, array<string, ' . Comment::class . '>>', Comment::find()->indexBy('user_id')->batch(250));
assertType(BatchQueryResult::class . '<int, array<string, array<string, mixed>>>', Comment::find()->asArray()->indexBy('user_id')->batch(250));

assertType(BatchQueryResult::class . '<int, array<int, ' . Comment::class . '>>', Comment::findBySql('')->batch(250));
assertType(BatchQueryResult::class . '<int, array<int, array<string, mixed>>>', Comment::findBySql('')->asArray()->batch(250));
assertType(BatchQueryResult::class . '<int, array<string, ' . Comment::class . '>>', Comment::findBySql('')->indexBy('user_id')->batch(250));
assertType(BatchQueryResult::class . '<int, array<string, array<string, mixed>>>', Comment::findBySql('')->asArray()->indexBy('user_id')->batch(250));

// Each
assertType(BatchQueryResult::class . '<int, ' . Comment::class . '>', Comment::find()->each(250));
assertType(BatchQueryResult::class . '<int, array<string, mixed>>', Comment::find()->asArray()->each(250));
assertType(BatchQueryResult::class . '<string, ' . Comment::class . '>', Comment::find()->indexBy('user_id')->each(250));
assertType(BatchQueryResult::class . '<string, array<string, mixed>>', Comment::find()->asArray()->indexBy('user_id')->each(250));

assertType(BatchQueryResult::class . '<int, ' . Comment::class . '>', Comment::findBySql('')->each(250));
assertType(BatchQueryResult::class . '<int, array<string, mixed>>', Comment::findBySql('')->asArray()->each(250));
assertType(BatchQueryResult::class . '<string, ' . Comment::class . '>', Comment::findBySql('')->indexBy('user_id')->each(250));
assertType(BatchQueryResult::class . '<string, array<string, mixed>>', Comment::findBySql('')->asArray()->indexBy('user_id')->each(250));
12 changes: 11 additions & 1 deletion tests/Type/_data/active-record-find-return-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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(''),
);
6 changes: 6 additions & 0 deletions tests/Type/_data/active-record-object-type.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down

0 comments on commit f841ad3

Please sign in to comment.