Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve replacement of return type for methods from Query\Builder #1575

Merged
merged 8 commits into from
Oct 31, 2024
32 changes: 29 additions & 3 deletions src/Alias.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Closure;
use Illuminate\Config\Repository as ConfigRepository;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Facades\Facade;
use ReflectionClass;
use Throwable;
Expand Down Expand Up @@ -333,7 +334,15 @@ protected function addMagicMethods()

if (!in_array($magic, $this->usedMethods)) {
if ($class !== $this->root) {
$this->methods[] = new Method($method, $this->alias, $class, $magic, $this->interfaces, $this->classAliases);
$this->methods[] = new Method(
$method,
$this->alias,
$class,
$magic,
$this->interfaces,
$this->classAliases,
$this->getReturnTypeNormalizers($class)
);
}
$this->usedMethods[] = $magic;
}
Expand Down Expand Up @@ -363,7 +372,8 @@ protected function detectMethods()
$reflection,
$method->name,
$this->interfaces,
$this->classAliases
$this->classAliases,
$this->getReturnTypeNormalizers($reflection)
);
}
$this->usedMethods[] = $method->name;
Expand All @@ -386,7 +396,8 @@ protected function detectMethods()
$reflection,
$macro_name,
$this->interfaces,
$this->classAliases
$this->classAliases,
$this->getReturnTypeNormalizers($reflection)
);
$this->usedMethods[] = $macro_name;
}
Expand All @@ -395,6 +406,21 @@ protected function detectMethods()
}
}

/**
* @param ReflectionClass $class
* @return array<string, string>
*/
protected function getReturnTypeNormalizers($class)
{
if ($this->alias === 'Eloquent' && in_array($class->getName(), [EloquentBuilder::class, QueryBuilder::class])) {
return [
'$this' => '\\' . EloquentBuilder::class . ($this->config->get('ide-helper.use_generics_annotations') ? '<static>' : '|static'),
];
}

return [];
}

/**
* @param $macro_func
*
Expand Down
6 changes: 4 additions & 2 deletions src/Macro.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,18 @@ class Macro extends Method
* @param null $methodName
* @param array $interfaces
* @param array $classAliases
* @param array $returnTypeNormalizers
*/
public function __construct(
$method,
$alias,
$class,
$methodName = null,
$interfaces = [],
$classAliases = []
$classAliases = [],
$returnTypeNormalizers = []
) {
parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases);
parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases, $returnTypeNormalizers);
}

/**
Expand Down
48 changes: 34 additions & 14 deletions src/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@
use Barryvdh\Reflection\DocBlock\Tag;
use Barryvdh\Reflection\DocBlock\Tag\ParamTag;
use Barryvdh\Reflection\DocBlock\Tag\ReturnTag;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;

class Method
{
Expand All @@ -39,6 +37,7 @@ class Method
protected $return = null;
protected $root;
protected $classAliases;
protected $returnTypeNormalizers;

/**
* @param \ReflectionMethod|\ReflectionFunctionAbstract $method
Expand All @@ -47,12 +46,14 @@ class Method
* @param string|null $methodName
* @param array $interfaces
* @param array $classAliases
* @param array $returnTypeNormalizers
*/
public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [])
public function __construct($method, $alias, $class, $methodName = null, $interfaces = [], array $classAliases = [], array $returnTypeNormalizers = [])
{
$this->method = $method;
$this->interfaces = $interfaces;
$this->classAliases = $classAliases;
$this->returnTypeNormalizers = $returnTypeNormalizers;
$this->name = $methodName ?: $method->name;
$this->real_name = $method->isClosure() ? $this->name : $method->name;
$this->initClassDefinedProperties($method, $class);
Expand Down Expand Up @@ -180,6 +181,25 @@ public function getParams($implode = true)
return $implode ? implode(', ', $this->params) : $this->params;
}

/**
* @param DocBlock|null $phpdoc
* @return ReturnTag|null
*/
public function getReturnTag($phpdoc = null)
{
if ($phpdoc === null) {
$phpdoc = $this->phpdoc;
}

$returnTags = $phpdoc->getTagsByName('return');

if (count($returnTags) === 0) {
return null;
}

return reset($returnTags);
}

/**
* Get the parameters for this method including default values
*
Expand Down Expand Up @@ -248,25 +268,31 @@ protected function normalizeParams(DocBlock $phpdoc)
}

/**
* Normalize the return tag (make full namespace, replace interfaces)
* Normalize the return tag (make full namespace, replace interfaces, resolve $this)
*
* @param DocBlock $phpdoc
*/
protected function normalizeReturn(DocBlock $phpdoc)
{
//Get the return type and adjust them for better autocomplete
$returnTags = $phpdoc->getTagsByName('return');
$tag = $this->getReturnTag($phpdoc);

if (count($returnTags) === 0) {
if ($tag === null) {
$this->return = null;
return;
}

/** @var ReturnTag $tag */
$tag = reset($returnTags);
// Get the expanded type
$returnValue = $tag->getType();

if (array_key_exists($returnValue, $this->returnTypeNormalizers)) {
$returnValue = $this->returnTypeNormalizers[$returnValue];
}

if ($returnValue === '$this') {
$returnValue = $this->root;
}

// Replace the interfaces
foreach ($this->interfaces as $interface => $real) {
$returnValue = str_replace($interface, $real, $returnValue);
Expand All @@ -275,12 +301,6 @@ protected function normalizeReturn(DocBlock $phpdoc)
// Set the changed content
$tag->setContent($returnValue . ' ' . $tag->getDescription());
$this->return = $returnValue;

if ($tag->getType() === '$this') {
Str::contains($this->root, Builder::class)
? $tag->setType($this->root . '|static')
: $tag->setType($this->root);
}
}

/**
Expand Down
72 changes: 68 additions & 4 deletions tests/MethodTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
namespace Barryvdh\LaravelIdeHelper\Tests;

use Barryvdh\LaravelIdeHelper\Method;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use PHPUnit\Framework\TestCase;

class MethodTest extends TestCase
Expand Down Expand Up @@ -54,11 +55,11 @@ public function testOutput()
}

/**
* Test the output of a class
* Test the output of Illuminate\Database\Eloquent\Builder
*/
public function testEloquentBuilderOutput()
{
$reflectionClass = new \ReflectionClass(Builder::class);
$reflectionClass = new \ReflectionClass(EloquentBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('upsert');

$method = new Method($reflectionMethod, 'Builder', $reflectionClass);
Expand All @@ -76,12 +77,75 @@ public function testEloquentBuilderOutput()
DOC;
$this->assertSame($output, $method->getDocComment(''));
$this->assertSame('upsert', $method->getName());
$this->assertSame('\\' . Builder::class, $method->getDeclaringClass());
$this->assertSame('\\' . EloquentBuilder::class, $method->getDeclaringClass());
$this->assertSame('$values, $uniqueBy, $update', $method->getParams(true));
$this->assertSame(['$values', '$uniqueBy', '$update'], $method->getParams(false));
$this->assertSame('$values, $uniqueBy, $update = null', $method->getParamsWithDefault(true));
$this->assertSame(['$values', '$uniqueBy', '$update = null'], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn());
$this->assertSame('int', rtrim($method->getReturnTag()->getType()));
}

/**
* Test normalized return type of Illuminate\Database\Eloquent\Builder
*/
public function testEloquentBuilderNormalizedReturnType()
{
$reflectionClass = new \ReflectionClass(EloquentBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('where');

$method = new Method($reflectionMethod, 'Builder', $reflectionClass, null, [], [], ['$this' => '\\' . EloquentBuilder::class . '<static>']);

$output = <<<'DOC'
/**
* Add a basic where clause to the query.
*
* @param (\Closure(static): mixed)|string|array|\Illuminate\Contracts\Database\Query\Expression $column
* @param mixed $operator
* @param mixed $value
* @param string $boolean
* @return \Illuminate\Database\Eloquent\Builder<static>
* @static
*/
DOC;
$this->assertSame($output, $method->getDocComment(''));
$this->assertSame('where', $method->getName());
$this->assertSame('\\' . EloquentBuilder::class, $method->getDeclaringClass());
$this->assertSame(['$column', '$operator', '$value', '$boolean'], $method->getParams(false));
$this->assertSame(['$column', '$operator = null', '$value = null', "\$boolean = 'and'"], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn());
$this->assertSame('\Illuminate\Database\Eloquent\Builder<static>', rtrim($method->getReturnTag()->getType()));
}

/**
* Test normalized return type of Illuminate\Database\Query\Builder
*/
public function testQueryBuilderNormalizedReturnType()
{
$reflectionClass = new \ReflectionClass(QueryBuilder::class);
$reflectionMethod = $reflectionClass->getMethod('whereNull');

$method = new Method($reflectionMethod, 'Builder', $reflectionClass, null, [], [], ['$this' => '\\' . EloquentBuilder::class . '<static>']);

$output = <<<'DOC'
/**
* Add a "where null" clause to the query.
*
* @param string|array|\Illuminate\Contracts\Database\Query\Expression $columns
* @param string $boolean
* @param bool $not
* @return \Illuminate\Database\Eloquent\Builder<static>
* @static
*/
DOC;

$this->assertSame($output, $method->getDocComment(''));
$this->assertSame('whereNull', $method->getName());
$this->assertSame('\\' . QueryBuilder::class, $method->getDeclaringClass());
$this->assertSame(['$columns', '$boolean', '$not'], $method->getParams(false));
$this->assertSame(['$columns', "\$boolean = 'and'", '$not = false'], $method->getParamsWithDefault(false));
$this->assertTrue($method->shouldReturn());
$this->assertSame('\Illuminate\Database\Eloquent\Builder<static>', rtrim($method->getReturnTag()->getType()));
}

/**
Expand Down