Skip to content

Commit

Permalink
feat: update Eloquent Builder stubs to be more specific (#1178)
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural committed Mar 18, 2022
1 parent f71d138 commit eef292f
Show file tree
Hide file tree
Showing 8 changed files with 490 additions and 14 deletions.
7 changes: 7 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
ignoreErrors:
-
message: "#^Creating new PHPStan\\\\Reflection\\\\Php\\\\DummyParameter is not covered by backward compatibility promise\\. The class might change in a minor PHPStan version\\.$#"
count: 1
path: src/Methods/ModelForwardsCallsExtension.php

47 changes: 39 additions & 8 deletions src/Methods/ModelForwardsCallsExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,26 @@

namespace NunoMaduro\Larastan\Methods;

use function array_map;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use NunoMaduro\Larastan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
Expand Down Expand Up @@ -106,7 +113,7 @@ public function __construct(ClassReflection $classReflection, string $methodName
$this->methodReflection = $methodReflection;
}

public function getDeclaringClass(): \PHPStan\Reflection\ClassReflection
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
Expand Down Expand Up @@ -136,7 +143,7 @@ public function getName(): string
return $this->methodName;
}

public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
public function getPrototype(): ClassMemberReflection
{
return $this;
}
Expand All @@ -146,7 +153,7 @@ public function getVariants(): array
return $this->methodReflection->getVariants();
}

public function isDeprecated(): \PHPStan\TrinaryLogic
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
Expand All @@ -156,22 +163,22 @@ public function getDeprecatedDescription(): ?string
return null;
}

public function isFinal(): \PHPStan\TrinaryLogic
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function isInternal(): \PHPStan\TrinaryLogic
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getThrowType(): ?\PHPStan\Type\Type
public function getThrowType(): ?Type
{
return null;
}

public function hasSideEffects(): \PHPStan\TrinaryLogic
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createYes();
}
Expand All @@ -184,7 +191,7 @@ public function hasSideEffects(): \PHPStan\TrinaryLogic
if ($builderReflection->hasNativeMethod($methodName)) {
$reflection = $builderReflection->getNativeMethod($methodName);

$parametersAcceptor = ParametersAcceptorSelector::selectSingle($reflection->getVariants());
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($this->transformStaticParameters($reflection, $genericBuilderAndModelType));

$returnType = TypeTraverser::map($parametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use ($genericBuilderAndModelType) {
if ($type instanceof TypeWithClassName && $type->getClassName() === Builder::class) {
Expand All @@ -209,4 +216,28 @@ public function hasSideEffects(): \PHPStan\TrinaryLogic

return null;
}

/**
* @return ParametersAcceptor[]
*/
private function transformStaticParameters(MethodReflection $method, GenericObjectType $builder): array
{
return array_map(function (ParametersAcceptor $acceptor) use ($builder): ParametersAcceptor {
return new FunctionVariant($acceptor->getTemplateTypeMap(), $acceptor->getResolvedTemplateTypeMap(), array_map(function (
ParameterReflection $parameter) use ($builder): ParameterReflection {
return new DummyParameter($parameter->getName(), $this->transformStaticType($parameter->getType(), $builder), $parameter->isOptional(), $parameter->passedByReference(), $parameter->isVariadic(), $parameter->getDefaultValue());
}, $acceptor->getParameters()), $acceptor->isVariadic(), $this->transformStaticType($acceptor->getReturnType(), $builder));
}, $method->getVariants());
}

private function transformStaticType(Type $type, GenericObjectType $builder): Type
{
return TypeTraverser::map($type, function (Type $type, callable $traverse) use ($builder): Type {
if ($type instanceof StaticType) {
return $builder;
}

return $traverse($type);
});
}
}
8 changes: 3 additions & 5 deletions stubs/EloquentBuilder.stub
Original file line number Diff line number Diff line change
Expand Up @@ -171,8 +171,6 @@ class Builder
*/
public function firstOr($columns = ['*'], \Closure $callback = null);



/**
* Add a relationship count / exists condition to the query.
*
Expand Down Expand Up @@ -219,7 +217,7 @@ class Builder
/**
* Add a basic where clause to the query.
*
* @param (\Closure(static<TModelClass>): void)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param (\Closure(static): void)|(\Closure(static): static)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
Expand All @@ -230,7 +228,7 @@ class Builder
/**
* Add an "or where" clause to the query.
*
* @param (\Closure(static<TModelClass>): void)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param (\Closure(static): void)|(\Closure(static): static)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @return $this
Expand Down Expand Up @@ -392,7 +390,7 @@ class Builder
/**
* Add a basic where clause to the query, and return the first result.
*
* @param (\Closure(static<TModelClass>): void)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param (\Closure(static): void)|(\Closure(static): static)|model-property<TModelClass>|array<model-property<TModelClass>|int, mixed>|\Illuminate\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
Expand Down
17 changes: 17 additions & 0 deletions tests/Application/app/Post.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,21 @@ public function users(): BelongsToMany
{
return $this->belongsToMany(User::class);
}

/**
* @return PostBuilder<Post>
*/
public static function query(): PostBuilder
{
return parent::query();
}

/**
* @param \Illuminate\Database\Query\Builder $query
* @return PostBuilder<Post>
*/
public function newEloquentBuilder($query): PostBuilder
{
return new PostBuilder($query);
}
}
13 changes: 13 additions & 0 deletions tests/Application/app/PostBuilder.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace App;

use Illuminate\Database\Eloquent\Builder;

/**
* @template TModelClass of Post
* @extends Builder<TModelClass>
*/
class PostBuilder extends Builder
{
}
134 changes: 134 additions & 0 deletions tests/Features/Methods/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@

namespace Tests\Features\Methods;

use App\Post;
use App\PostBuilder;
use App\User;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Collection;
use function PHPStan\Testing\assertType;

\Illuminate\Database\Eloquent\Builder::macro('globalCustomMacro', function (string $arg): string {
return $arg;
Expand Down Expand Up @@ -229,4 +232,135 @@ public function testRestore()
{
return User::query()->restore();
}

public function testRelationMethods(): void
{
User::query()->has('accounts', '=', 1, 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->has('users', '=', 1, 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->doesntHave('accounts', 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->doesntHave('users', 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->whereHas('accounts', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->whereHas('users', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->orWhereHas('accounts', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->orWhereHas('users', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->hasMorph('accounts', [], '=', 1, 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->hasMorph('users', [], '=', 1, 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->doesntHaveMorph('accounts', [], 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->doesntHaveMorph('users', [], 'and', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->whereHasMorph('accounts', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->whereHasMorph('users', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->orWhereHasMorph('accounts', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->orWhereHasMorph('users', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->whereDoesntHaveMorph('accounts', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->whereDoesntHaveMorph('users', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->orWhereDoesntHaveMorph('accounts', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->orWhereDoesntHaveMorph('users', [], function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->whereDoesntHave('accounts', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->whereDoesntHave('users', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->orWhereDoesntHave('accounts', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\Account>', $query);
});

Post::query()->orWhereDoesntHave('users', function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder', $query);
//assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

User::query()->firstWhere(function (EloquentBuilder $query) {
assertType('Illuminate\Database\Eloquent\Builder<App\User>', $query);
});

Post::query()->firstWhere(function (PostBuilder $query) {
assertType('App\PostBuilder<App\Post>', $query);
});
}
}
Loading

0 comments on commit eef292f

Please sign in to comment.