Skip to content

Commit

Permalink
feat: support scopes defined in model docblocks
Browse files Browse the repository at this point in the history
  • Loading branch information
canvural committed Sep 22, 2021
1 parent 2137fbd commit 88422fb
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
23 changes: 23 additions & 0 deletions src/Methods/BuilderHelper.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use NunoMaduro\Larastan\Reflection\AnnotationScopeMethodParameterReflection;
use NunoMaduro\Larastan\Reflection\AnnotationScopeMethodReflection;
use NunoMaduro\Larastan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariantWithPhpDocs;
Expand Down Expand Up @@ -140,6 +142,27 @@ public function dynamicWhere(
public function searchOnEloquentBuilder(ClassReflection $eloquentBuilder, string $methodName, ClassReflection $model): ?MethodReflection
{
// Check for local query scopes
if (array_key_exists('scope'.ucfirst($methodName), $model->getMethodTags())) {
$methodTag = $model->getMethodTags()['scope'.ucfirst($methodName)];

$parameters = [];
foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
$parameters[] = new AnnotationScopeMethodParameterReflection($parameterName, $parameterTag->getType(), $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), $parameterTag->getDefaultValue());
}

// We shift the parameters,
// because first parameter is the Builder
array_shift($parameters);

return new EloquentBuilderMethodReflection(
'scope'.ucfirst($methodName),
$model,
new AnnotationScopeMethodReflection('scope'.ucfirst($methodName), $model, $methodTag->getReturnType(), $parameters, $methodTag->isStatic(), false),
$parameters,
$methodTag->getReturnType()
);
}

if ($model->hasNativeMethod('scope'.ucfirst($methodName))) {
$methodReflection = $model->getNativeMethod('scope'.ucfirst($methodName));
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
Expand Down
70 changes: 70 additions & 0 deletions src/Reflection/AnnotationScopeMethodParameterReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Reflection;

use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\Type;

final class AnnotationScopeMethodParameterReflection implements ParameterReflection
{
/** @var string */
private $name;

/** @var Type */
private $type;

/** @var PassedByReference */
private $passedByReference;

/** @var bool */
private $isOptional;

/** @var bool */
private $isVariadic;

/** @var Type|null */
private $defaultValue;

public function __construct(string $name, Type $type, PassedByReference $passedByReference, bool $isOptional, bool $isVariadic, ?Type $defaultValue)
{
$this->name = $name;
$this->type = $type;
$this->passedByReference = $passedByReference;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
$this->defaultValue = $defaultValue;
}

public function getName() : string
{
return $this->name;
}

public function isOptional() : bool
{
return $this->isOptional;
}

public function getType() : Type
{
return $this->type;
}

public function passedByReference() : PassedByReference
{
return $this->passedByReference;
}

public function isVariadic() : bool
{
return $this->isVariadic;
}

public function getDefaultValue() : ?Type
{
return $this->defaultValue;
}
}
131 changes: 131 additions & 0 deletions src/Reflection/AnnotationScopeMethodReflection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php

declare(strict_types=1);

namespace NunoMaduro\Larastan\Reflection;

use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;

final class AnnotationScopeMethodReflection implements MethodReflection
{
/** @var string */
private $name;

/** @var ClassReflection */
private $declaringClass;

/** @var Type */
private $returnType;

/** @var bool */
private $isStatic;

/** @var AnnotationScopeMethodParameterReflection[] */
private $parameters;

/** @var bool */
private $isVariadic;

/** @var FunctionVariant[]|null */
private $variants = null;

/**
* @param string $name
* @param ClassReflection $declaringClass
* @param Type $returnType
* @param AnnotationScopeMethodParameterReflection[] $parameters
* @param bool $isStatic
* @param bool $isVariadic
*/
public function __construct(string $name, ClassReflection $declaringClass, Type $returnType, array $parameters, bool $isStatic, bool $isVariadic)
{
$this->name = $name;
$this->declaringClass = $declaringClass;
$this->returnType = $returnType;
$this->parameters = $parameters;
$this->isStatic = $isStatic;
$this->isVariadic = $isVariadic;
}

public function getDeclaringClass() : ClassReflection
{
return $this->declaringClass;
}

public function getPrototype() : ClassMemberReflection
{
return $this;
}

public function isStatic() : bool
{
return $this->isStatic;
}

public function isPrivate() : bool
{
return false;
}

public function isPublic() : bool
{
return true;
}

public function getName() : string
{
return $this->name;
}
/**
* @return ParametersAcceptor[]
*/
public function getVariants() : array
{
if ($this->variants === null) {
$this->variants = [new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->parameters, $this->isVariadic, $this->returnType)];
}
return $this->variants;
}

public function isDeprecated() : TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getDeprecatedDescription() : ?string
{
return null;
}

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

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

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

public function hasSideEffects() : TrinaryLogic
{
return TrinaryLogic::createMaybe();
}

public function getDocComment() : ?string
{
return null;
}
}
3 changes: 3 additions & 0 deletions tests/Application/app/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

/**
* @property string $propertyDefinedOnlyInAnnotation
*
* @method Builder<static> scopeSomeScope(Builder $builder)
*
* @mixin \Eloquent
*/
class User extends Authenticatable
Expand Down
9 changes: 9 additions & 0 deletions tests/Features/Models/Scopes.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasOne;
use function PHPStan\Testing\assertType;

class Scopes extends Model
{
Expand Down Expand Up @@ -70,4 +71,12 @@ public function testScopeThatStartsWithWordWhere(): Builder
{
return User::query()->whereActive();
}

public function testScopeDefinedInClassDocBlock(User $user): void
{
assertType('Illuminate\Database\Eloquent\Builder<App\User>', $user->someScope());
assertType('Illuminate\Database\Eloquent\Builder<App\User>', User::query()->someScope());
assertType('Illuminate\Database\Eloquent\Builder<App\User>', $user->where('foo')->someScope());
assertType('Illuminate\Database\Eloquent\Collection<App\User>', $user->where('foo')->someScope()->get());
}
}

0 comments on commit 88422fb

Please sign in to comment.