Skip to content

Commit f14d274

Browse files
committed
Basic setup
1 parent 9f70c7a commit f14d274

File tree

4 files changed

+290
-0
lines changed

4 files changed

+290
-0
lines changed

composer.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,34 @@
11
{
2+
"name": "knuckleswtf/scribe-docblock2attributes",
3+
"license": "MIT",
4+
"description": "Automatically convert Scribe v3 docblock annotations to PHP 8 attributes",
5+
"keywords": [
6+
"API",
7+
"documentation",
8+
"laravel",
9+
"dingo"
10+
],
11+
"homepage": "http://github.com/knuckleswtf/scribe-docblock2attributes",
12+
"authors": [
13+
{
14+
"name": "Shalvah",
15+
"email": "[email protected]"
16+
}
17+
],
218
"require": {
19+
"php": ">=8.0",
320
"knuckleswtf/scribe": "v4.x-dev"
21+
},
22+
"autoload": {
23+
"psr-4": {
24+
"Knuckles\\Scribe\\Docblock2Attributes\\": "src/"
25+
}
26+
},
27+
"extra": {
28+
"laravel": {
29+
"providers": [
30+
"Knuckles\\Scribe\\Docblock2Attributes\\ServiceProvider"
31+
]
32+
}
433
}
534
}

src/Commands/Process.php

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes\Commands;
4+
5+
use Illuminate\Console\Command;
6+
use Illuminate\Support\Str;
7+
use Knuckles\Scribe\Docblock2Attributes\Extractor;
8+
use Knuckles\Scribe\GroupedEndpoints\GroupedEndpointsFactory;
9+
use Knuckles\Scribe\Matching\RouteMatcherInterface;
10+
use Knuckles\Scribe\Tools\ConsoleOutputUtils as c;
11+
use Knuckles\Scribe\Tools\DocumentationConfig;
12+
use Knuckles\Scribe\Tools\ErrorHandlingUtils as e;
13+
use Knuckles\Scribe\Tools\Globals;
14+
use Knuckles\Scribe\Tools\Utils as u;
15+
use Mpociot\Reflection\DocBlock;
16+
use Mpociot\Reflection\DocBlock\Tag;
17+
use ReflectionClass;
18+
19+
class Process extends Command
20+
{
21+
protected $signature = "scribe:docblock2attributes
22+
{--config=scribe : choose which config file to use}
23+
";
24+
25+
protected $description = 'Convert docblock tags to PHP attributes.';
26+
27+
protected DocumentationConfig $docConfig;
28+
29+
public function handle(RouteMatcherInterface $routeMatcher, GroupedEndpointsFactory $groupedEndpointsFactory): void
30+
{
31+
$this->bootstrap();
32+
33+
if (!empty($this->docConfig->get("default_group"))) {
34+
$this->warn("It looks like you just upgraded to Scribe v4.");
35+
$this->warn("Please run the upgrade command first: `php artisan scribe:upgrade`.");
36+
exit(1);
37+
}
38+
39+
$routes = $routeMatcher->getRoutes($this->docConfig->get('routes', []), $this->docConfig->get('router'));
40+
$extracted = $this->extract($routes);
41+
// $this->transform($routes);
42+
}
43+
44+
protected function extract(array $matches): array
45+
{
46+
$extractor = new Extractor($this->docConfig);
47+
$parsedEndpoints = [];
48+
49+
foreach ($matches as $routeItem) {
50+
$route = $routeItem->getRoute();
51+
52+
$routeControllerAndMethod = u::getRouteClassAndMethodNames($route);
53+
if (!$this->isValidRoute($routeControllerAndMethod)) {
54+
c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route));
55+
continue;
56+
}
57+
58+
if (!$this->doesControllerMethodExist($routeControllerAndMethod)) {
59+
c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.');
60+
continue;
61+
}
62+
63+
if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) {
64+
c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.');
65+
continue;
66+
}
67+
68+
try {
69+
c::info('Processing route: ' . c::getRouteRepresentation($route));
70+
[$controllerName, $methodName] = u::getRouteClassAndMethodNames($route);
71+
$controller = new ReflectionClass($controllerName);
72+
$method = u::getReflectedRouteMethod([$controllerName, $methodName]);
73+
$endpointData = $extractor->processRoute($route, $method, $controller);
74+
ray($endpointData);
75+
c::success('Processed route: ' . c::getRouteRepresentation($route));
76+
} catch (\Exception $exception) {
77+
$this->encounteredErrors = true;
78+
c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.');
79+
e::dumpExceptionIfVerbose($exception);
80+
}
81+
}
82+
83+
return $parsedEndpoints;
84+
}
85+
86+
protected function transform(array $matches): array
87+
{
88+
$extractor = new Transformer($this->docConfig);
89+
$parsedEndpoints = [];
90+
91+
foreach ($matches as $routeItem) {
92+
$route = $routeItem->getRoute();
93+
94+
$routeControllerAndMethod = u::getRouteClassAndMethodNames($route);
95+
if (!$this->isValidRoute($routeControllerAndMethod)) {
96+
c::warn('Skipping invalid route: ' . c::getRouteRepresentation($route));
97+
continue;
98+
}
99+
100+
if (!$this->doesControllerMethodExist($routeControllerAndMethod)) {
101+
c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ' - Controller method does not exist.');
102+
continue;
103+
}
104+
105+
if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) {
106+
c::warn('Skipping route: ' . c::getRouteRepresentation($route) . ': @hideFromAPIDocumentation was specified.');
107+
continue;
108+
}
109+
110+
try {
111+
c::info('Processing route: ' . c::getRouteRepresentation($route));
112+
$endpointData = $extractor->processRoute($route, $routeItem->getRules());
113+
c::success('Processed route: ' . c::getRouteRepresentation($route));
114+
} catch (\Exception $exception) {
115+
$this->encounteredErrors = true;
116+
c::error('Failed processing route: ' . c::getRouteRepresentation($route) . ' - Exception encountered.');
117+
e::dumpExceptionIfVerbose($exception);
118+
}
119+
}
120+
121+
return $parsedEndpoints;
122+
}
123+
124+
125+
private function isValidRoute(array $routeControllerAndMethod = null): bool
126+
{
127+
if (is_array($routeControllerAndMethod)) {
128+
[$classOrObject, $method] = $routeControllerAndMethod;
129+
if (u::isInvokableObject($classOrObject)) {
130+
return true;
131+
}
132+
$routeControllerAndMethod = $classOrObject . '@' . $method;
133+
}
134+
135+
return !is_callable($routeControllerAndMethod) && !is_null($routeControllerAndMethod);
136+
}
137+
138+
private function doesControllerMethodExist(array $routeControllerAndMethod): bool
139+
{
140+
[$class, $method] = $routeControllerAndMethod;
141+
$reflection = new ReflectionClass($class);
142+
143+
if ($reflection->hasMethod($method)) {
144+
return true;
145+
}
146+
147+
return false;
148+
}
149+
150+
private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod): bool
151+
{
152+
if (!($class = $routeControllerAndMethod[0]) instanceof \Closure) {
153+
$classDocBlock = new DocBlock((new ReflectionClass($class))->getDocComment() ?: '');
154+
$shouldIgnoreClass = collect($classDocBlock->getTags())
155+
->filter(function (Tag $tag) {
156+
return Str::lower($tag->getName()) === 'hidefromapidocumentation';
157+
})->isNotEmpty();
158+
159+
if ($shouldIgnoreClass) {
160+
return true;
161+
}
162+
}
163+
164+
$methodDocBlock = new DocBlock(u::getReflectedRouteMethod($routeControllerAndMethod)->getDocComment() ?: '');
165+
$shouldIgnoreMethod = collect($methodDocBlock->getTags())
166+
->filter(function (Tag $tag) {
167+
return Str::lower($tag->getName()) === 'hidefromapidocumentation';
168+
})->isNotEmpty();
169+
170+
return $shouldIgnoreMethod;
171+
}
172+
173+
174+
public function bootstrap(): void
175+
{
176+
// The --verbose option is included with all Artisan commands.
177+
Globals::$shouldBeVerbose = $this->option('verbose');
178+
179+
c::bootstrapOutput($this->output);
180+
181+
$this->configName = $this->option('config');
182+
if (!config($this->configName)) {
183+
throw new \InvalidArgumentException("The specified config (config/{$this->configName}.php) doesn't exist.");
184+
}
185+
186+
$this->docConfig = new DocumentationConfig(config($this->configName));
187+
}
188+
189+
}

src/Extractor.php

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes;
4+
5+
use Illuminate\Routing\Route;
6+
use Knuckles\Scribe\Extracting\FindsFormRequestForMethod;
7+
use Knuckles\Scribe\Extracting\RouteDocBlocker;
8+
use Knuckles\Scribe\Tools\DocumentationConfig;
9+
use Mpociot\Reflection\DocBlock;
10+
use ReflectionClass;
11+
use ReflectionFunctionAbstract;
12+
13+
class Extractor
14+
{
15+
use FindsFormRequestForMethod;
16+
17+
public function __construct(public DocumentationConfig $config)
18+
{
19+
}
20+
21+
public function processRoute(Route $route, ReflectionFunctionAbstract $method, ?ReflectionClass $controller): array
22+
{
23+
$tags = [];
24+
if ($formRequestClass = $this->getFormRequestReflectionClass($method)) {
25+
$tagsFromFormRequest = $this->parseData(new DocBlock($formRequestClass->getDocComment()));
26+
$tags[$formRequestClass] = $tagsFromFormRequest;
27+
}
28+
29+
$tags[$controller->name] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['class']);
30+
31+
$tags[$controller->name.'/'.$method->name] ??= $this->parseData(RouteDocBlocker::getDocBlocksFromRoute($route)['method']);
32+
33+
return $tags;
34+
}
35+
36+
protected function parseData(DocBlock $docblock)
37+
{
38+
/** @var \Mpociot\Reflection\DocBlock\Tag[] $tags */
39+
$tags = $docblock->getTags() ?? [];
40+
41+
$parsed = [];
42+
foreach ($tags as $tag) {
43+
$parsed[] = match(strtolower($tag->getName())) {
44+
'bodyparam' => $tag->getContent()
45+
};
46+
}
47+
48+
return $parsed;
49+
}
50+
}

src/ServiceProvider.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Knuckles\Scribe\Docblock2Attributes;
4+
5+
use Illuminate\Support\ServiceProvider as BaseSP;
6+
use Knuckles\Scribe\Docblock2Attributes\Commands\Process;
7+
use Knuckles\Scribe\Matching\RouteMatcher;
8+
use Knuckles\Scribe\Matching\RouteMatcherInterface;
9+
10+
class ServiceProvider extends BaseSP
11+
{
12+
public function boot()
13+
{
14+
if ($this->app->runningInConsole()) {
15+
$this->commands([
16+
Process::class,
17+
]);
18+
}
19+
20+
$this->app->bind(RouteMatcherInterface::class, config('scribe.routeMatcher', RouteMatcher::class));
21+
}
22+
}

0 commit comments

Comments
 (0)