From 84f0894cf58d06f0d583f3b000d188b6d6602d78 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Thu, 24 Apr 2025 16:26:37 -0400 Subject: [PATCH 1/3] Generate PHPDocs for Scopes Would resolve #25. **Note:** also adjusts spacing before `*` in docblocks so that the `*`s are aligned --- src/Services/GeneratePhpDocService.php | 11 ++-- .../Generators/AccessorsGenerator.php | 4 +- src/Services/Generators/ColumnsGenerator.php | 4 +- .../Generators/RelationshipsGenerator.php | 4 +- src/Services/Generators/ScopesGenerator.php | 37 +++++++++++++ .../Units/Generators/ScopesGeneratorTest.php | 54 +++++++++++++++++++ 6 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 src/Services/Generators/ScopesGenerator.php create mode 100644 tests/Units/Generators/ScopesGeneratorTest.php diff --git a/src/Services/GeneratePhpDocService.php b/src/Services/GeneratePhpDocService.php index 1a6ca07..4334e01 100644 --- a/src/Services/GeneratePhpDocService.php +++ b/src/Services/GeneratePhpDocService.php @@ -9,6 +9,7 @@ use SethPhat\EloquentDocs\Services\Generators\PhpDocGeneratorContract; use SethPhat\EloquentDocs\Services\Generators\RelationshipsGenerator; use SethPhat\EloquentDocs\Services\Generators\TableGenerator; +use SethPhat\EloquentDocs\Services\Generators\ScopesGenerator; class GeneratePhpDocService { @@ -17,6 +18,7 @@ class GeneratePhpDocService ColumnsGenerator::class, RelationshipsGenerator::class, AccessorsGenerator::class, + ScopesGenerator::class, ]; protected Model $model; @@ -56,11 +58,14 @@ public function generate(): string */ $generator = $this->laravel->make($generatorClass); - $phpDocStr .= $generator->generate($this->model, $this->options); + $doc = $generator->generate($this->model, $this->options); + if ($doc) { + $phpDocStr .= "$doc\n *\n"; + } } - $phpDocStr .= "\n*/"; + $phpDocStr .= "\n */"; return $phpDocStr; } -} \ No newline at end of file +} diff --git a/src/Services/Generators/AccessorsGenerator.php b/src/Services/Generators/AccessorsGenerator.php index 4b547a5..0622182 100644 --- a/src/Services/Generators/AccessorsGenerator.php +++ b/src/Services/Generators/AccessorsGenerator.php @@ -20,7 +20,7 @@ class AccessorsGenerator implements PhpDocGeneratorContract public function generate(Model $model, array $options = []): string { - $phpDocStr = "\n*\n* === Accessors/Attributes ==="; + $phpDocStr = "\n * === Accessors/Attributes ==="; $virtualAttributes = $this->getVirtualAttributes($model); if ($virtualAttributes->isEmpty()) { @@ -98,4 +98,4 @@ protected function getVirtualAttributes(Model $model): Collection ]) ->values(); } -} \ No newline at end of file +} diff --git a/src/Services/Generators/ColumnsGenerator.php b/src/Services/Generators/ColumnsGenerator.php index 671db30..e1ba21a 100644 --- a/src/Services/Generators/ColumnsGenerator.php +++ b/src/Services/Generators/ColumnsGenerator.php @@ -27,7 +27,7 @@ public function generate(Model $model, array $options = []): string } // columns - $phpDocStr = "\n*\n* === Columns ==="; + $phpDocStr = "\n * === Columns ==="; foreach ($columns as $column) { $phpDocStr .= sprintf( '%s * @property %s %s', @@ -116,4 +116,4 @@ protected function getJsonCastType(string $column): string return 'string'; } -} \ No newline at end of file +} diff --git a/src/Services/Generators/RelationshipsGenerator.php b/src/Services/Generators/RelationshipsGenerator.php index e858d3a..b45316b 100644 --- a/src/Services/Generators/RelationshipsGenerator.php +++ b/src/Services/Generators/RelationshipsGenerator.php @@ -26,7 +26,7 @@ class RelationshipsGenerator implements PhpDocGeneratorContract public function generate(Model $model, array $options = []): string { - $phpDocStr = "\n*\n* === Relationships ==="; + $phpDocStr = "\n * === Relationships ==="; $relationships = $this->getRelations($model); $isUseShortClass = $options['useShortClass'] ?? false; @@ -99,4 +99,4 @@ protected function getRelations($model) ->filter() ->values(); } -} \ No newline at end of file +} diff --git a/src/Services/Generators/ScopesGenerator.php b/src/Services/Generators/ScopesGenerator.php new file mode 100644 index 0000000..5093996 --- /dev/null +++ b/src/Services/Generators/ScopesGenerator.php @@ -0,0 +1,37 @@ +getMethods(ReflectionMethod::IS_PUBLIC) as $method) { + if ($this->isLocalScope($method)) { + // Remove 'scope' from method name and convert to camelCase + $scopeName = lcfirst(substr($method->getName(), 5)); + $phpDoc .= " * @method static \\Illuminate\\Database\\Eloquent\\Builder<{$className}> {$scopeName}()\n"; + } + } + + if (!$phpDoc) { + return ""; + } + + return "\n * === Scopes ===\n$phpDoc"; + } + + private function isLocalScope(ReflectionMethod $method): bool + { + $methodName = $method->getName(); + return str_starts_with($methodName, 'scope') && strlen($methodName) > 5; + } +} diff --git a/tests/Units/Generators/ScopesGeneratorTest.php b/tests/Units/Generators/ScopesGeneratorTest.php new file mode 100644 index 0000000..9cd0220 --- /dev/null +++ b/tests/Units/Generators/ScopesGeneratorTest.php @@ -0,0 +1,54 @@ +where('votes', '>', 100); + } + public function scopeActive($query) + { + $query->where('status', '=', 'active'); + } +} + +class ScopesTestModel2 extends Model { + public function nonScopeMethod() + { + return 'This is not a scope'; + } +} + +class ScopesGeneratorTest extends TestCase +{ + public function testGenerateIncludesLocalScopes(): void + { + + $model = new ScopesTestModel1(); + + $generator = new ScopesGenerator(); + $phpDoc = $generator->generate($model, []); + + print("phpdoc:\n$phpDoc"); + + $this->assertStringContainsString('=== Scopes ===', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder popular()', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder active()', $phpDoc); + } + + public function testGenerateExcludesNonScopeMethods(): void + { + $model = new ScopesTestModel2(); + + $generator = new ScopesGenerator(); + $phpDoc = $generator->generate($model, []); + + $this->assertStringNotContainsString('=== Scopes ===', $phpDoc); + $this->assertStringNotContainsString('@method static \Illuminate\Database\Eloquent\Builder nonScopeMethod()', $phpDoc); + } +} From 90238ab7d6277c3042507e24b0acc393645d22c7 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Thu, 24 Apr 2025 16:50:39 -0400 Subject: [PATCH 2/3] Fix spacing --- src/Services/GeneratePhpDocService.php | 16 +++++----------- src/Services/Generators/ScopesGenerator.php | 2 +- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/Services/GeneratePhpDocService.php b/src/Services/GeneratePhpDocService.php index 4334e01..d1e9630 100644 --- a/src/Services/GeneratePhpDocService.php +++ b/src/Services/GeneratePhpDocService.php @@ -50,22 +50,16 @@ public function setOptions(array $options): self */ public function generate(): string { - $phpDocStr = '/**'; - - foreach (static::GENERATORS as $generatorClass) { + $phpDocStr = "/**\n " + . join("\n *\n ", array_filter(array_map(function (string $generatorClass): string { /** * @var PhpDocGeneratorContract $generator */ $generator = $this->laravel->make($generatorClass); - $doc = $generator->generate($this->model, $this->options); - if ($doc) { - $phpDocStr .= "$doc\n *\n"; - } - } - - $phpDocStr .= "\n */"; - + return trim($generator->generate($this->model, $this->options)); + }, static::GENERATORS), fn ($item) => !empty($item))) + . "\n */"; return $phpDocStr; } } diff --git a/src/Services/Generators/ScopesGenerator.php b/src/Services/Generators/ScopesGenerator.php index 5093996..4fcf424 100644 --- a/src/Services/Generators/ScopesGenerator.php +++ b/src/Services/Generators/ScopesGenerator.php @@ -26,7 +26,7 @@ public function generate(Model $model, array $options = []): string return ""; } - return "\n * === Scopes ===\n$phpDoc"; + return " * === Scopes ===\n$phpDoc"; } private function isLocalScope(ReflectionMethod $method): bool From 795dde0bbf4d5cdefacb6222f6a88f9ad8898440 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Thu, 24 Apr 2025 17:26:15 -0400 Subject: [PATCH 3/3] Add and expand params --- src/Services/Generators/ScopesGenerator.php | 28 ++++++++++++++++++- .../Units/Generators/ScopesGeneratorTest.php | 24 ++++++++++++++-- 2 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Services/Generators/ScopesGenerator.php b/src/Services/Generators/ScopesGenerator.php index 4fcf424..4ce0970 100644 --- a/src/Services/Generators/ScopesGenerator.php +++ b/src/Services/Generators/ScopesGenerator.php @@ -2,9 +2,11 @@ namespace SethPhat\EloquentDocs\Services\Generators; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use ReflectionClass; use ReflectionMethod; +use ReflectionNamedType; class ScopesGenerator implements PhpDocGeneratorContract { @@ -18,7 +20,31 @@ public function generate(Model $model, array $options = []): string if ($this->isLocalScope($method)) { // Remove 'scope' from method name and convert to camelCase $scopeName = lcfirst(substr($method->getName(), 5)); - $phpDoc .= " * @method static \\Illuminate\\Database\\Eloquent\\Builder<{$className}> {$scopeName}()\n"; + $args=""; + $i = -1; + $args = join(", ", array_filter(array_map(function ($param) use (&$i) { + $type = $param->getType(); + $typeDoc = ""; + $i += 1; + print("$i: $param\n $type\n"); + if ($type) { + if ($type instanceof ReflectionNamedType && $type->getName() === Builder::class) { + return null; + } + $typeDoc = "$type "; + } else if ($i === 0) { + // First argument is the query builder + return null; + } + if ($param->isPassedByReference()) { + $typeDoc .= "&"; + } + if ($param->isVariadic()) { + $typeDoc .= "..."; + } + return $typeDoc . '$' . $param->getName(); + }, $method->getParameters()), fn ($item) => !empty($item))); + $phpDoc .= " * @method static \\Illuminate\\Database\\Eloquent\\Builder<{$className}> {$scopeName}($args)\n"; } } diff --git a/tests/Units/Generators/ScopesGeneratorTest.php b/tests/Units/Generators/ScopesGeneratorTest.php index 9cd0220..692b746 100644 --- a/tests/Units/Generators/ScopesGeneratorTest.php +++ b/tests/Units/Generators/ScopesGeneratorTest.php @@ -2,15 +2,32 @@ namespace Tests\Units\Generators; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use PHPUnit\Framework\TestCase; use SethPhat\EloquentDocs\Services\Generators\ScopesGenerator; class ScopesTestModel1 extends Model { - public function scopePopular($query) + public function scopePopular1(Builder $query, ...$args) { $query->where('votes', '>', 100); } + + public function scopePopular2(Builder $query, array ...$args) + { + $query->where('votes', '>', 100); + } + + public function scopeUnpopular(Builder $query, ?int $votes = null) + { + $query->where('votes', '<=', $votes); + } + + public function scopeInactive(Builder $query, bool &$foo) + { + $query->where('status', '!=', 'active'); + } + public function scopeActive($query) { $query->where('status', '=', 'active'); @@ -37,8 +54,11 @@ public function testGenerateIncludesLocalScopes(): void print("phpdoc:\n$phpDoc"); $this->assertStringContainsString('=== Scopes ===', $phpDoc); - $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder popular()', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder popular1(...$args)', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder popular2(array ...$args)', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder unpopular(?int $votes)', $phpDoc); $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder active()', $phpDoc); + $this->assertStringContainsString('@method static \Illuminate\Database\Eloquent\Builder inactive(bool &$foo)', $phpDoc); } public function testGenerateExcludesNonScopeMethods(): void