diff --git a/src/Alias.php b/src/Alias.php index 046fffe41..74bd8cec5 100644 --- a/src/Alias.php +++ b/src/Alias.php @@ -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; @@ -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; } @@ -363,7 +372,8 @@ protected function detectMethods() $reflection, $method->name, $this->interfaces, - $this->classAliases + $this->classAliases, + $this->getReturnTypeNormalizers($reflection) ); } $this->usedMethods[] = $method->name; @@ -386,7 +396,8 @@ protected function detectMethods() $reflection, $macro_name, $this->interfaces, - $this->classAliases + $this->classAliases, + $this->getReturnTypeNormalizers($reflection) ); $this->usedMethods[] = $macro_name; } @@ -395,6 +406,21 @@ protected function detectMethods() } } + /** + * @param ReflectionClass $class + * @return array + */ + 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'), + ]; + } + + return []; + } + /** * @param $macro_func * diff --git a/src/Macro.php b/src/Macro.php index 77cc5a6e2..3679d79fb 100644 --- a/src/Macro.php +++ b/src/Macro.php @@ -18,6 +18,7 @@ class Macro extends Method * @param null $methodName * @param array $interfaces * @param array $classAliases + * @param array $returnTypeNormalizers */ public function __construct( $method, @@ -25,9 +26,10 @@ public function __construct( $class, $methodName = null, $interfaces = [], - $classAliases = [] + $classAliases = [], + $returnTypeNormalizers = [] ) { - parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases); + parent::__construct($method, $alias, $class, $methodName, $interfaces, $classAliases, $returnTypeNormalizers); } /** diff --git a/src/Method.php b/src/Method.php index a46e3f2f3..b4e0f7003 100644 --- a/src/Method.php +++ b/src/Method.php @@ -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 { @@ -39,6 +37,7 @@ class Method protected $return = null; protected $root; protected $classAliases; + protected $returnTypeNormalizers; /** * @param \ReflectionMethod|\ReflectionFunctionAbstract $method @@ -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); @@ -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 * @@ -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); @@ -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); - } } /** diff --git a/tests/MethodTest.php b/tests/MethodTest.php index c740f03bf..c1681da62 100644 --- a/tests/MethodTest.php +++ b/tests/MethodTest.php @@ -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 @@ -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); @@ -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 . '']); + + $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 + */ +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', 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 . '']); + + $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 + */ +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', rtrim($method->getReturnTag()->getType())); } /**