diff --git a/src/Commands/GenerateDocumentation.php b/src/Commands/GenerateDocumentation.php index b7aeb69..3ee9e0d 100644 --- a/src/Commands/GenerateDocumentation.php +++ b/src/Commands/GenerateDocumentation.php @@ -6,17 +6,20 @@ use Illuminate\Console\Command; use Illuminate\Routing\Route; use Illuminate\Support\Collection; use Illuminate\Support\Facades\URL; +use Illuminate\Support\Str; use Knuckles\Scribe\Extracting\Generator; use Knuckles\Scribe\Matching\Match; use Knuckles\Scribe\Matching\RouteMatcherInterface; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; use Knuckles\Scribe\Tools\DocumentationConfig; +use Knuckles\Scribe\Tools\ErrorHandlingUtils as e; use Knuckles\Scribe\Tools\Flags; -use Knuckles\Scribe\Tools\Utils; +use Knuckles\Scribe\Tools\Utils as u; use Knuckles\Scribe\Writing\Writer; use Mpociot\Reflection\DocBlock; +use Mpociot\Reflection\DocBlock\Tag; use ReflectionClass; use ReflectionException; -use Shalvah\Clara\Clara; class GenerateDocumentation extends Command { @@ -46,11 +49,6 @@ class GenerateDocumentation extends Command */ private $baseUrl; - /** - * @var Clara - */ - private $clara; - /** * Execute the console command. * @@ -72,53 +70,48 @@ class GenerateDocumentation extends Command /* @var $group Collection */ return $group->first()['metadata']['groupName']; }, SORT_NATURAL); - $writer = new Writer( - $this->docConfig, - $this->option('force'), - $this->clara - ); + + $writer = new Writer($this->docConfig, $this->option('force')); $writer->writeDocs($groupedRoutes); } /** - * @param Match[] $routes + * @param Match[] $matches * * @return array *@throws \ReflectionException * */ - private function processRoutes(array $routes) + private function processRoutes(array $matches) { $generator = new Generator($this->docConfig); $parsedRoutes = []; - foreach ($routes as $routeItem) { - $route = $routeItem->getRoute(); + foreach ($matches as $routeItem) { /** @var Route $route */ - $messageFormat = '%s route: [%s] %s'; - $routeMethods = implode(',', $generator->getMethods($route)); - $routePath = $generator->getUri($route); + $route = $routeItem->getRoute(); - $routeControllerAndMethod = Utils::getRouteClassAndMethodNames($route->getAction()); + $routeControllerAndMethod = u::getRouteClassAndMethodNames($route); if (! $this->isValidRoute($routeControllerAndMethod)) { - $this->clara->warn(sprintf($messageFormat, 'Skipping invalid', $routeMethods, $routePath)); + c::warn('Skipping invalid route: '. c::getRouteRepresentation($route)); continue; } if (! $this->doesControllerMethodExist($routeControllerAndMethod)) { - $this->clara->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': Controller method does not exist.'); + c::warn('Skipping route: '. c::getRouteRepresentation($route).' - Controller method does not exist.'); continue; } if ($this->isRouteHiddenFromDocumentation($routeControllerAndMethod)) { - $this->clara->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . ': @hideFromAPIDocumentation was specified.'); + c::warn('Skipping route: '. c::getRouteRepresentation($route). ': @hideFromAPIDocumentation was specified.'); continue; } try { $parsedRoutes[] = $generator->processRoute($route, $routeItem->getRules()); - $this->clara->info(sprintf($messageFormat, 'Processed', $routeMethods, $routePath)); + c::info('Processed route: '. c::getRouteRepresentation($route)); } catch (\Exception $exception) { - $this->clara->warn(sprintf($messageFormat, 'Skipping', $routeMethods, $routePath) . '- Exception ' . get_class($exception) . ' encountered : ' . $exception->getMessage()); + c::warn('Skipping route: '. c::getRouteRepresentation($route) . ' - Exception encountered.'); + e::dumpExceptionIfVerbose($exception); } } @@ -134,7 +127,7 @@ class GenerateDocumentation extends Command { if (is_array($routeControllerAndMethod)) { [$classOrObject, $method] = $routeControllerAndMethod; - if (Utils::isInvokableObject($classOrObject)) { + if (u::isInvokableObject($classOrObject)) { return true; } $routeControllerAndMethod = $classOrObject . '@' . $method; @@ -155,11 +148,11 @@ class GenerateDocumentation extends Command [$class, $method] = $routeControllerAndMethod; $reflection = new ReflectionClass($class); - if (! $reflection->hasMethod($method)) { - return false; + if ($reflection->hasMethod($method)) { + return true; } - return true; + return false; } /** @@ -171,30 +164,27 @@ class GenerateDocumentation extends Command */ private function isRouteHiddenFromDocumentation(array $routeControllerAndMethod) { - $comment = Utils::reflectRouteMethod($routeControllerAndMethod)->getDocComment(); + $comment = u::reflectRouteMethod($routeControllerAndMethod)->getDocComment(); - if ($comment) { - $phpdoc = new DocBlock($comment); - - return collect($phpdoc->getTags()) - ->filter(function ($tag) { - return $tag->getName() === 'hideFromAPIDocumentation'; - }) - ->isEmpty(); + if (!$comment) { + return false; } - return true; + $phpdoc = new DocBlock($comment); + + return collect($phpdoc->getTags()) + ->filter(function (Tag $tag) { + return Str::lower($tag->getName()) === 'hidefromapidocumentation'; + })->isNotEmpty(); } public function bootstrap(): void { - // Using a global static variable here, so fuck off if you don't like it. + // Using a global static variable here, so 🙄 if you don't like it. // Also, the --verbose option is included with all Artisan commands. Flags::$shouldBeVerbose = $this->option('verbose'); - $this->clara = clara('knuckleswtf/scribe', Flags::$shouldBeVerbose) - ->useOutput($this->output) - ->only(); + c::bootstrapOutput($this->output); $this->docConfig = new DocumentationConfig(config('scribe')); $this->baseUrl = $this->docConfig->get('base_url') ?? config('app.url'); diff --git a/src/Extracting/Generator.php b/src/Extracting/Generator.php index 9d18e12..25207cd 100644 --- a/src/Extracting/Generator.php +++ b/src/Extracting/Generator.php @@ -8,7 +8,7 @@ use Illuminate\Support\Arr; use Illuminate\Support\Str; use Knuckles\Scribe\Extracting\Strategies\Strategy; use Knuckles\Scribe\Tools\DocumentationConfig; -use Knuckles\Scribe\Tools\Utils; +use Knuckles\Scribe\Tools\Utils as u; use ReflectionClass; use ReflectionFunctionAbstract; @@ -55,9 +55,9 @@ class Generator */ public function processRoute(Route $route, array $routeRules = []) { - [$controllerName, $methodName] = Utils::getRouteClassAndMethodNames($route->getAction()); + [$controllerName, $methodName] = u::getRouteClassAndMethodNames($route); $controller = new ReflectionClass($controllerName); - $method = Utils::reflectRouteMethod([$controllerName, $methodName]); + $method = u::reflectRouteMethod([$controllerName, $methodName]); $parsedRoute = [ 'id' => md5($this->getUri($route) . ':' . implode($this->getMethods($route))), @@ -70,7 +70,7 @@ class Generator $urlParameters = $this->fetchUrlParameters($controller, $method, $route, $routeRules, $parsedRoute); $parsedRoute['urlParameters'] = $urlParameters; $parsedRoute['cleanUrlParameters'] = self::cleanParams($urlParameters); - $parsedRoute['boundUri'] = Utils::getFullUrl($route, $parsedRoute['cleanUrlParameters']); + $parsedRoute['boundUri'] = u::getFullUrl($route, $parsedRoute['cleanUrlParameters']); $parsedRoute = $this->addAuthField($parsedRoute); diff --git a/src/Extracting/RouteDocBlocker.php b/src/Extracting/RouteDocBlocker.php index 3d27840..9f4142a 100644 --- a/src/Extracting/RouteDocBlocker.php +++ b/src/Extracting/RouteDocBlocker.php @@ -3,7 +3,8 @@ namespace Knuckles\Scribe\Extracting; use Illuminate\Routing\Route; -use Knuckles\Scribe\Tools\Utils; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; +use Knuckles\Scribe\Tools\Utils as u; use Mpociot\Reflection\DocBlock; use ReflectionClass; @@ -26,7 +27,7 @@ class RouteDocBlocker */ public static function getDocBlocksFromRoute(Route $route): array { - list($className, $methodName) = Utils::getRouteClassAndMethodNames($route); + [$className, $methodName] = u::getRouteClassAndMethodNames($route); $normalizedClassName = static::normalizeClassName($className); $docBlocks = self::getCachedDocBlock($route, $normalizedClassName, $methodName); @@ -37,10 +38,10 @@ class RouteDocBlocker $class = new ReflectionClass($className); if (! $class->hasMethod($methodName)) { - throw new \Exception("Error while fetching docblock for route: Class $className does not contain method $methodName"); + throw new \Exception("Error while fetching docblock for route ". c::getRouteRepresentation($route).": Class $className does not contain method $methodName"); } - $method = Utils::reflectRouteMethod([$className, $methodName]); + $method = u::reflectRouteMethod([$className, $methodName]); $docBlocks = [ 'method' => new DocBlock($method->getDocComment() ?: ''), diff --git a/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php b/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php index 471df69..623b7e9 100644 --- a/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php +++ b/src/Extracting/Strategies/BodyParameters/GetFromFormRequest.php @@ -9,12 +9,11 @@ use Illuminate\Support\Arr; use Illuminate\Support\Facades\Validator; use Illuminate\Support\Str; use Illuminate\Contracts\Validation\Rule; -use Knuckles\Scribe\Extracting\BodyParameterDefinition; use Knuckles\Scribe\Extracting\ParamHelpers; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Extracting\ValidationRuleDescriptionParser as Description; -use Knuckles\Scribe\Tools\Utils; -use Knuckles\Scribe\Tools\WritingUtils; +use Knuckles\Scribe\Extracting\ValidationRuleDescriptionParser as d; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; +use Knuckles\Scribe\Tools\WritingUtils as w; use ReflectionClass; use ReflectionException; use ReflectionFunctionAbstract; @@ -93,7 +92,8 @@ class GetFromFormRequest extends Strategy return call_user_func_array([$formRequest, 'bodyParameters'], []); } - clara('knuckleswtf/scribe')->warn("No bodyParameters() method found in ".get_class($formRequest)." Scribe will only be able to extract basic information from the rules() method."); + c::warn("No bodyParameters() method found in ".get_class($formRequest)." Scribe will only be able to extract basic information from the rules() method."); + return []; } @@ -105,7 +105,7 @@ class GetFromFormRequest extends Strategy $parameters = []; foreach ($rules as $parameter => $ruleset) { if (count($customParameterData) && !isset($customParameterData[$parameter])) { - clara('knuckleswtf/scribe')->warn("No data found for parameter '$parameter' from your bodyParameters() method. Add an entry for '$parameter' so you can add description and example."); + c::debug("No data found for parameter '$parameter' from your bodyParameters() method. Add an entry for '$parameter' so you can add description and example."); } $parameterInfo = $customParameterData[$parameter] ?? []; @@ -251,11 +251,11 @@ class GetFromFormRequest extends Strategy */ case 'timezone': // Laravel's message merely says "The value must be a valid zone" - $parameterData['description'] .= "The value must be a valid time zone, such as `Africa/Accra`. "; + $parameterData['description'] .= "The value must be a valid time zone, such as Africa/Accra. "; $parameterData['value'] = $this->getFaker()->timezone; break; case 'email': - $parameterData['description'] .= Description::getDescription($rule).' '; + $parameterData['description'] .= d::getDescription($rule).' '; $parameterData['value'] = $this->getFaker()->safeEmail; $parameterData['type'] = 'string'; break; @@ -266,18 +266,18 @@ class GetFromFormRequest extends Strategy $parameterData['description'] .= "The value must be a valid URL. "; break; case 'ip': - $parameterData['description'] .= Description::getDescription($rule).' '; + $parameterData['description'] .= d::getDescription($rule).' '; $parameterData['value'] = $this->getFaker()->ipv4; $parameterData['type'] = 'string'; break; case 'json': $parameterData['type'] = 'string'; - $parameterData['description'] .= Description::getDescription($rule).' '; + $parameterData['description'] .= d::getDescription($rule).' '; $parameterData['value'] = json_encode([$this->getFaker()->word, $this->getFaker()->word,]); break; case 'date': $parameterData['type'] = 'string'; - $parameterData['description'] .= Description::getDescription($rule).' '; + $parameterData['description'] .= d::getDescription($rule).' '; $parameterData['value'] = date(\DateTime::ISO8601, time()); break; case 'date_format': @@ -313,7 +313,7 @@ class GetFromFormRequest extends Strategy */ case 'image': $parameterData['type'] = 'file'; - $parameterData['description'] .= Description::getDescription($rule).' '; + $parameterData['description'] .= d::getDescription($rule).' '; break; /** @@ -321,7 +321,7 @@ class GetFromFormRequest extends Strategy */ case 'in': // Not using the rule description here because it only says "The attribute is invalid" - $description = 'The value must be one of '.WritingUtils::getListOfValuesAsFriendlyHtmlString($arguments); + $description = 'The value must be one of '. w::getListOfValuesAsFriendlyHtmlString($arguments); $parameterData['description'] .= $description.' '; $parameterData['value'] = Arr::random($arguments); break; diff --git a/src/Extracting/Strategies/Responses/ResponseCalls.php b/src/Extracting/Strategies/Responses/ResponseCalls.php index eca61f2..31ec81c 100644 --- a/src/Extracting/Strategies/Responses/ResponseCalls.php +++ b/src/Extracting/Strategies/Responses/ResponseCalls.php @@ -13,8 +13,8 @@ use Illuminate\Support\Str; use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers; use Knuckles\Scribe\Extracting\ParamHelpers; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Tools\ErrorHandlingUtils; -use Knuckles\Scribe\Tools\Flags; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; +use Knuckles\Scribe\Tools\ErrorHandlingUtils as e; use Knuckles\Scribe\Tools\Utils; use ReflectionClass; use ReflectionFunctionAbstract; @@ -70,12 +70,9 @@ class ResponseCalls extends Strategy ], ]; } catch (Exception $e) { - clara('knuckleswtf/scribe')->warn('Exception thrown during response call for [' . implode(',', $route->methods) . "] {$route->uri}."); - if (Flags::$shouldBeVerbose) { - ErrorHandlingUtils::dumpException($e); - } else { - clara('knuckleswtf/scribe')->warn("Run this again with the --verbose flag to see the exception."); - } + c::warn('Exception thrown during response call for [' . implode(',', $route->methods) . "] {$route->uri}."); + e::dumpExceptionIfVerbose($e); + $response = null; } finally { $this->finish(); diff --git a/src/Extracting/Strategies/Responses/UseApiResourceTags.php b/src/Extracting/Strategies/Responses/UseApiResourceTags.php index ed0c5a2..d6ca5cf 100644 --- a/src/Extracting/Strategies/Responses/UseApiResourceTags.php +++ b/src/Extracting/Strategies/Responses/UseApiResourceTags.php @@ -15,11 +15,9 @@ use Illuminate\Support\Arr; use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers; use Knuckles\Scribe\Extracting\RouteDocBlocker; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Tools\AnnotationParser; -use Knuckles\Scribe\Tools\ErrorHandlingUtils; -use Knuckles\Scribe\Tools\Flags; -use Knuckles\Scribe\Tools\Utils; -use League\Fractal\Resource\Collection; +use Knuckles\Scribe\Tools\AnnotationParser as a; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; +use Knuckles\Scribe\Tools\ErrorHandlingUtils as e; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; use ReflectionClass; @@ -52,13 +50,8 @@ class UseApiResourceTags extends Strategy try { return $this->getApiResourceResponse($methodDocBlock->getTags()); } catch (Exception $e) { - clara('knuckleswtf/scribe')->warn('Exception thrown when fetching Eloquent API resource response for [' . implode(',', $route->methods) . "] {$route->uri}."); - if (Flags::$shouldBeVerbose) { - ErrorHandlingUtils::dumpException($e); - } else { - clara('knuckleswtf/scribe')->warn("Run this again with the --verbose flag to see the exception."); - } - + c::warn('Exception thrown when fetching Eloquent API resource response for [' . implode(',', $route->methods) . "] {$route->uri}."); + e::dumpExceptionIfVerbose($e); return null; } } @@ -95,7 +88,7 @@ class UseApiResourceTags extends Strategy if (count($pagination) == 1) { $perPage = $pagination[0]; $paginator = new LengthAwarePaginator( - // For some reason, the LengthAware paginator needs only first page items to work correctly + // For some reason, the LengthAware paginator needs only first page items to work correctly collect($models)->slice(0, $perPage), count($models), $perPage @@ -151,7 +144,7 @@ class UseApiResourceTags extends Strategy $relations = []; $pagination = []; if ($modelTag) { - ['content' => $type, 'attributes' => $attributes] = AnnotationParser::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with', 'paginate']); + ['content' => $type, 'attributes' => $attributes] = a::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with', 'paginate']); $states = $attributes['states'] ? explode(',', $attributes['states']) : []; $relations = $attributes['with'] ? explode(',', $attributes['with']) : []; $pagination = $attributes['paginate'] ? explode(',', $attributes['paginate']) : []; @@ -193,9 +186,8 @@ class UseApiResourceTags extends Strategy return $factory->make(); } } catch (Exception $e) { - if (Flags::$shouldBeVerbose) { - clara('knuckleswtf/scribe')->warn("Eloquent model factory failed to instantiate {$type}; trying to fetch from database."); - } + c::debug("Eloquent model factory failed to instantiate {$type}; trying to fetch from database."); + e::dumpExceptionIfVerbose($e); $instance = new $type(); if ($instance instanceof \Illuminate\Database\Eloquent\Model) { @@ -207,9 +199,8 @@ class UseApiResourceTags extends Strategy } } catch (Exception $e) { // okay, we'll stick with `new` - if (Flags::$shouldBeVerbose) { - clara('knuckleswtf/scribe')->warn("Failed to fetch first {$type} from database; using `new` to instantiate."); - } + c::debug("Failed to fetch first {$type} from database; using `new` to instantiate."); + e::dumpExceptionIfVerbose($e); } } } finally { diff --git a/src/Extracting/Strategies/Responses/UseResponseFileTag.php b/src/Extracting/Strategies/Responses/UseResponseFileTag.php index 8f17a7e..a09a084 100644 --- a/src/Extracting/Strategies/Responses/UseResponseFileTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseFileTag.php @@ -5,7 +5,8 @@ namespace Knuckles\Scribe\Extracting\Strategies\Responses; use Illuminate\Routing\Route; use Knuckles\Scribe\Extracting\RouteDocBlocker; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Tools\AnnotationParser; +use Knuckles\Scribe\Tools\AnnotationParser as a; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; @@ -59,14 +60,14 @@ class UseResponseFileTag extends Strategy [$_, $status, $mainContent] = $result; $json = $result[3] ?? null; - ['attributes' => $attributes, 'content' => $relativeFilePath] = AnnotationParser::parseIntoContentAndAttributes($mainContent, ['status', 'scenario']); + ['attributes' => $attributes, 'content' => $relativeFilePath] = a::parseIntoContentAndAttributes($mainContent, ['status', 'scenario']); $status = $attributes['status'] ?: ($status ?: 200); $description = $attributes['scenario'] ? "$status, {$attributes['scenario']}" : "$status"; $filePath = storage_path($relativeFilePath); if (! file_exists($filePath)) { - throw new \Exception('@responseFile ' . $relativeFilePath . ' does not exist'); + c::warn("@responseFile {$relativeFilePath} does not exist"); } $content = file_get_contents($filePath, true); if ($json) { diff --git a/src/Extracting/Strategies/Responses/UseResponseTag.php b/src/Extracting/Strategies/Responses/UseResponseTag.php index 0fa2a01..b95aeff 100644 --- a/src/Extracting/Strategies/Responses/UseResponseTag.php +++ b/src/Extracting/Strategies/Responses/UseResponseTag.php @@ -5,7 +5,7 @@ namespace Knuckles\Scribe\Extracting\Strategies\Responses; use Illuminate\Routing\Route; use Knuckles\Scribe\Extracting\RouteDocBlocker; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Tools\AnnotationParser; +use Knuckles\Scribe\Tools\AnnotationParser as a; use Mpociot\Reflection\DocBlock; use Mpociot\Reflection\DocBlock\Tag; @@ -60,7 +60,7 @@ class UseResponseTag extends Strategy $status = $result[1] ?: 200; $content = $result[2] ?: '{}'; - ['attributes' => $attributes, 'content' => $content] = AnnotationParser::parseIntoContentAndAttributes($content, ['status', 'scenario']); + ['attributes' => $attributes, 'content' => $content] = a::parseIntoContentAndAttributes($content, ['status', 'scenario']); $status = $attributes['status'] ?: $status; $description = $attributes['scenario'] ? "$status, {$attributes['scenario']}" : $status; diff --git a/src/Extracting/Strategies/Responses/UseTransformerTags.php b/src/Extracting/Strategies/Responses/UseTransformerTags.php index afa5d92..504eb04 100644 --- a/src/Extracting/Strategies/Responses/UseTransformerTags.php +++ b/src/Extracting/Strategies/Responses/UseTransformerTags.php @@ -10,10 +10,9 @@ use Illuminate\Support\Arr; use Knuckles\Scribe\Extracting\DatabaseTransactionHelpers; use Knuckles\Scribe\Extracting\RouteDocBlocker; use Knuckles\Scribe\Extracting\Strategies\Strategy; -use Knuckles\Scribe\Tools\AnnotationParser; -use Knuckles\Scribe\Tools\ErrorHandlingUtils; -use Knuckles\Scribe\Tools\Flags; -use Knuckles\Scribe\Tools\Utils; +use Knuckles\Scribe\Tools\AnnotationParser as a; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; +use Knuckles\Scribe\Tools\ErrorHandlingUtils as e; use League\Fractal\Manager; use League\Fractal\Resource\Collection; use League\Fractal\Resource\Item; @@ -36,9 +35,9 @@ class UseTransformerTags extends Strategy * @param array $rulesToApply * @param array $context * + * @return array|null * @throws \Exception * - * @return array|null */ public function __invoke(Route $route, ReflectionClass $controller, ReflectionFunctionAbstract $method, array $rulesToApply, array $context = []) { @@ -49,12 +48,8 @@ class UseTransformerTags extends Strategy try { return $this->getTransformerResponse($methodDocBlock->getTags()); } catch (Exception $e) { - clara('knuckleswtf/scribe')->warn('Exception thrown when fetching transformer response for [' . implode(',', $route->methods) . "] {$route->uri}."); - if (Flags::$shouldBeVerbose) { - ErrorHandlingUtils::dumpException($e); - } else { - clara('knuckleswtf/scribe')->warn("Run this again with the --verbose flag to see the exception."); - } + c::warn('Exception thrown when fetching transformer response for [' . implode(',', $route->methods) . "] {$route->uri}."); + e::dumpExceptionIfVerbose($e); return null; } @@ -79,7 +74,7 @@ class UseTransformerTags extends Strategy $fractal = new Manager(); - if (! is_null($this->config->get('fractal.serializer'))) { + if (!is_null($this->config->get('fractal.serializer'))) { $fractal->setSerializer(app($this->config->get('fractal.serializer'))); } @@ -103,11 +98,11 @@ class UseTransformerTags extends Strategy $response = response($fractal->createData($resource)->toJson()); return [ - [ - 'status' => $statusCode ?: 200, - 'content' => $response->getContent(), - ], - ]; + [ + 'status' => $statusCode ?: 200, + 'content' => $response->getContent(), + ], + ]; } /** @@ -129,9 +124,9 @@ class UseTransformerTags extends Strategy * @param array $tags * @param ReflectionFunctionAbstract $transformerMethod * + * @return array * @throws Exception * - * @return array */ private function getClassToBeTransformed(array $tags, ReflectionFunctionAbstract $transformerMethod): array { @@ -143,12 +138,12 @@ class UseTransformerTags extends Strategy $states = []; $relations = []; if ($modelTag) { - ['content' => $type, 'attributes' => $attributes] = AnnotationParser::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with']); + ['content' => $type, 'attributes' => $attributes] = a::parseIntoContentAndAttributes($modelTag->getContent(), ['states', 'with']); $states = $attributes['states'] ? explode(',', $attributes['states']) : []; $relations = $attributes['with'] ? explode(',', $attributes['with']) : []; } else { $parameter = Arr::first($transformerMethod->getParameters()); - if ($parameter->hasType() && ! $parameter->getType()->isBuiltin() && class_exists($parameter->getType()->getName())) { + if ($parameter->hasType() && !$parameter->getType()->isBuiltin() && class_exists($parameter->getType()->getName())) { // Ladies and gentlemen, we have a type! $type = $parameter->getType()->getName(); } @@ -183,9 +178,8 @@ class UseTransformerTags extends Strategy return $factory->make(); } } catch (Exception $e) { - if (Flags::$shouldBeVerbose) { - clara('knuckleswtf/scribe')->warn("Eloquent model factory failed to instantiate {$type}; trying to fetch from database."); - } + c::debug("Eloquent model factory failed to instantiate {$type}; trying to fetch from database."); + e::dumpExceptionIfVerbose($e); $instance = new $type(); if ($instance instanceof IlluminateModel) { @@ -197,9 +191,8 @@ class UseTransformerTags extends Strategy } } catch (Exception $e) { // okay, we'll stick with `new` - if (Flags::$shouldBeVerbose) { - clara('knuckleswtf/scribe')->warn("Failed to fetch first {$type} from database; using `new` to instantiate."); - } + c::debug("Failed to fetch first {$type} from database; using `new` to instantiate."); + e::dumpExceptionIfVerbose($e); } } } finally { diff --git a/src/Scribe.php b/src/Scribe.php deleted file mode 100644 index 7fe4556..0000000 --- a/src/Scribe.php +++ /dev/null @@ -1,16 +0,0 @@ -useOutput($outputInterface) + ->only(); + } + + public static function warn($message) + { + if (!self::$clara) { + self::bootstrapOutput(new ConsoleOutput); + } + self::$clara->warn($message); + } + + public static function info($message) + { + if (!self::$clara) { + self::bootstrapOutput(new ConsoleOutput); + } + self::$clara->info($message); + } + + public static function debug($message) + { + if (!self::$clara) { + self::bootstrapOutput(new ConsoleOutput); + } + self::$clara->debug($message); + } + + public static function success($message) + { + if (!self::$clara) { + self::bootstrapOutput(new ConsoleOutput); + } + self::$clara->success($message); + } + + /** + * Return a string representation of a route to output to the console eg [GET] /api/users + * @param Route $route + * + * @return string + */ + public static function getRouteRepresentation(Route $route): string + { + $routeMethods = implode(',', array_diff($route->methods(), ['HEAD'])); + $routePath = $route->uri(); + return "[$routeMethods] $routePath"; + } +} diff --git a/src/Tools/ErrorHandlingUtils.php b/src/Tools/ErrorHandlingUtils.php index 599c0f8..0c43567 100644 --- a/src/Tools/ErrorHandlingUtils.php +++ b/src/Tools/ErrorHandlingUtils.php @@ -7,14 +7,19 @@ use Symfony\Component\Console\Output\OutputInterface; class ErrorHandlingUtils { - public static function dumpException(\Throwable $e): void + public static function dumpExceptionIfVerbose(\Throwable $e): void { - if (!class_exists(\NunoMaduro\Collision\Handler::class)) { - dump($e); - ConsoleOutputUtils::info("You can get better exception output by installing the library nunomaduro/collision."); - return; + if (Flags::$shouldBeVerbose) { + self::dumpException($e); + } else { + ConsoleOutputUtils::warn(get_class($e) . ': ' . $e->getMessage()); + ConsoleOutputUtils::warn('Run again with --verbose for a full stacktrace'); } + } + + public static function dumpException(\Throwable $e): void + { $output = new ConsoleOutput(OutputInterface::VERBOSITY_VERBOSE); try { $handler = new \NunoMaduro\Collision\Handler(new \NunoMaduro\Collision\Writer(null, $output)); diff --git a/src/Tools/Utils.php b/src/Tools/Utils.php index c7f9adf..60bc228 100644 --- a/src/Tools/Utils.php +++ b/src/Tools/Utils.php @@ -5,15 +5,13 @@ namespace Knuckles\Scribe\Tools; use Closure; use Exception; use Illuminate\Routing\Route; +use Knuckles\Scribe\Tools\ConsoleOutputUtils as c; use League\Flysystem\Adapter\Local; use League\Flysystem\Filesystem; use ReflectionClass; use ReflectionException; use ReflectionFunction; use ReflectionFunctionAbstract; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\VarExporter\VarExporter; class Utils { @@ -24,16 +22,9 @@ class Utils return self::replaceUrlParameterPlaceholdersWithValues($uri, $urlParameters); } - /** - * @param array|Route $routeOrAction - * - * @return array|null - */ - public static function getRouteClassAndMethodNames($routeOrAction) + public static function getRouteClassAndMethodNames(Route $route): array { - $action = $routeOrAction instanceof Route - ? $routeOrAction->getAction() - : $routeOrAction; + $action = $route->getAction(); $uses = $action['uses']; @@ -53,7 +44,7 @@ class Utils ]; } - throw new Exception("Couldn't handle route."); + throw new Exception("Couldn't get class and method names for route ". c::getRouteRepresentation($route).'.'); } /** diff --git a/src/Tools/WritingUtils.php b/src/Tools/WritingUtils.php index dacc3a5..89fe249 100644 --- a/src/Tools/WritingUtils.php +++ b/src/Tools/WritingUtils.php @@ -2,17 +2,6 @@ namespace Knuckles\Scribe\Tools; -use Closure; -use Exception; -use Illuminate\Routing\Route; -use League\Flysystem\Adapter\Local; -use League\Flysystem\Filesystem; -use ReflectionClass; -use ReflectionException; -use ReflectionFunction; -use ReflectionFunctionAbstract; -use Symfony\Component\Console\Output\ConsoleOutput; -use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\VarExporter\VarExporter; class WritingUtils diff --git a/src/Writing/PostmanCollectionWriter.php b/src/Writing/PostmanCollectionWriter.php index 522c21c..d688014 100644 --- a/src/Writing/PostmanCollectionWriter.php +++ b/src/Writing/PostmanCollectionWriter.php @@ -135,7 +135,7 @@ class PostmanCollectionWriter return [ 'key' => $key, 'value' => urlencode($parameterData['value']), - 'description' => $parameterData['description'], + 'description' => strip_tags($parameterData['description']), // Default query params to disabled if they aren't required and have empty values 'disabled' => !($parameterData['required'] ?? false) && empty($parameterData['value']), ]; diff --git a/src/Writing/Writer.php b/src/Writing/Writer.php index b4f8a25..79ae232 100644 --- a/src/Writing/Writer.php +++ b/src/Writing/Writer.php @@ -2,24 +2,17 @@ namespace Knuckles\Scribe\Writing; -use Illuminate\Console\Command; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; use Knuckles\Pastel\Pastel; +use Knuckles\Scribe\Tools\ConsoleOutputUtils; use Knuckles\Scribe\Tools\DocumentationConfig; -use Knuckles\Scribe\Tools\Flags; use Knuckles\Scribe\Tools\Utils; -use Shalvah\Clara\Clara; class Writer { - /** - * @var Clara - */ - protected $clara; - /** * @var DocumentationConfig */ @@ -70,13 +63,12 @@ class Writer */ private $lastTimesWeModifiedTheseFiles; - public function __construct(DocumentationConfig $config = null, bool $forceIt = false, $clara = null) + public function __construct(DocumentationConfig $config = null, bool $forceIt = false) { // If no config is injected, pull from global $this->config = $config ?: new DocumentationConfig(config('scribe')); $this->baseUrl = $this->config->get('base_url') ?? config('app.url'); $this->forceIt = $forceIt; - $this->clara = $clara ?: clara('knuckleswtf/scribe', Flags::$shouldBeVerbose)->only(); $this->shouldGeneratePostmanCollection = $this->config->get('postman.enabled', false); $this->pastel = new Pastel(); $this->isStatic = $this->config->get('type') === 'static'; @@ -114,7 +106,7 @@ class Writer 'title' => config('app.name', '') . ' API Documentation', ]; - $this->clara->info('Writing source Markdown files to: ' . $this->sourceOutputPath); + ConsoleOutputUtils::info('Writing source Markdown files to: ' . $this->sourceOutputPath); if (!is_dir($this->sourceOutputPath)) { mkdir($this->sourceOutputPath, 0777, true); @@ -124,7 +116,7 @@ class Writer $this->writeAuthMarkdownFile(); $this->writeRoutesMarkdownFile($parsedRoutes, $settings); - $this->clara->info('Wrote source Markdown files to: ' . $this->sourceOutputPath); + ConsoleOutputUtils::info('Wrote source Markdown files to: ' . $this->sourceOutputPath); } public function generateMarkdownOutputForEachRoute(Collection $parsedRoutes, array $settings): Collection @@ -156,7 +148,7 @@ class Writer protected function writePostmanCollection(Collection $parsedRoutes): void { if ($this->shouldGeneratePostmanCollection) { - $this->clara->info('Generating Postman collection'); + ConsoleOutputUtils::info('Generating Postman collection'); $collection = $this->generatePostmanCollection($parsedRoutes); if ($this->isStatic) { @@ -167,7 +159,7 @@ class Writer $collectionPath = 'storage/app/scribe/collection.json'; } - $this->clara->success("Wrote Postman collection to: {$collectionPath}"); + ConsoleOutputUtils::success("Wrote Postman collection to: {$collectionPath}"); } } @@ -213,7 +205,7 @@ class Writer public function writeHtmlDocs(): void { - $this->clara->info('Generating API HTML code'); + ConsoleOutputUtils::info('Generating API HTML code'); $this->pastel->generate($this->sourceOutputPath . '/index.md', 'public/docs'); @@ -221,7 +213,7 @@ class Writer $this->performFinalTasksForLaravelType(); } - $this->clara->success("Wrote HTML documentation to: {$this->outputPath}"); + ConsoleOutputUtils::success("Wrote HTML documentation to: {$this->outputPath}"); } protected function writeIndexMarkdownFile(array $settings): void @@ -307,9 +299,9 @@ class Writer if ($this->hasFileBeenModified($routeGroupMarkdownFile)) { if ($this->forceIt) { - $this->clara->warn("Discarded manual changes for file $routeGroupMarkdownFile"); + ConsoleOutputUtils::warn("Discarded manual changes for file $routeGroupMarkdownFile"); } else { - $this->clara->warn("Skipping modified file $routeGroupMarkdownFile"); + ConsoleOutputUtils::warn("Skipping modified file $routeGroupMarkdownFile"); return; } } diff --git a/tests/GenerateDocumentationTest.php b/tests/GenerateDocumentationTest.php index b6158fd..284c5af 100644 --- a/tests/GenerateDocumentationTest.php +++ b/tests/GenerateDocumentationTest.php @@ -129,7 +129,7 @@ class GenerateDocumentationTest extends TestCase } /** @test */ - public function can_skip_non_existent_response_files() + public function can_skip_nonexistent_response_files() { RouteFacade::get('/api/non-existent', TestController::class . '@withNonExistentResponseFile'); diff --git a/tests/Strategies/BodyParameters/GetFromFormRequestTest.php b/tests/Strategies/BodyParameters/GetFromFormRequestTest.php index 3ffd5d2..23e74e0 100644 --- a/tests/Strategies/BodyParameters/GetFromFormRequestTest.php +++ b/tests/Strategies/BodyParameters/GetFromFormRequestTest.php @@ -191,7 +191,7 @@ class GetFromFormRequestTest extends TestCase ['timezone' => 'timezone|required'], ['timezone' => ['description' => $description]], [ - 'description' => 'The value must be a valid time zone, such as `Africa/Accra`.', + 'description' => 'The value must be a valid time zone, such as Africa/Accra.', 'type' => 'string', ], ], @@ -247,7 +247,7 @@ class GetFromFormRequestTest extends TestCase ['in' => 'in:3,5,6|required'], ['in' => ['description' => $description]], [ - 'description' => 'The value must be one of `3`, `5`, or `6`.', + 'description' => 'The value must be one of 3, 5, or 6.', 'type' => 'string', ], ], diff --git a/tests/Strategies/Responses/ResponseCallsTest.php b/tests/Strategies/Responses/ResponseCallsTest.php index cb07147..32cf597 100644 --- a/tests/Strategies/Responses/ResponseCallsTest.php +++ b/tests/Strategies/Responses/ResponseCallsTest.php @@ -254,6 +254,23 @@ class ResponseCallsTest extends TestCase $this->assertNull($results); } + /** @test */ + public function does_not_make_response_call_if_forbidden_by_config() + { + $route = LaravelRouteFacade::post('/shouldFetchRouteResponse', [TestController::class, 'shouldFetchRouteResponse']); + + $rules = [ + 'response_calls' => [ + 'methods' => [], + ], + ]; + $context = ['responses' => []]; + $strategy = new ResponseCalls(new DocumentationConfig([])); + $results = $strategy->makeResponseCallIfEnabledAndNoSuccessResponses($route, $rules, $context); + + $this->assertNull($results); + } + public function registerDingoRoute(string $httpMethod, string $path, string $controllerMethod) { $desiredRoute = null; diff --git a/tests/TestHelpers.php b/tests/TestHelpers.php index 5b2256b..926a72e 100644 --- a/tests/TestHelpers.php +++ b/tests/TestHelpers.php @@ -14,9 +14,11 @@ trait TestHelpers */ public function artisan($command, $parameters = []) { - $this->app[Kernel::class]->call($command, $parameters); + /** @var Kernel $kernel */ + $kernel = $this->app[Kernel::class]; + $kernel->call($command, $parameters); - return $this->app[Kernel::class]->output(); + return $kernel->output(); } private function assertFilesHaveSameContent($pathToExpected, $pathToActual) diff --git a/todo.md b/todo.md index 7ff65fd..706d6f2 100644 --- a/todo.md +++ b/todo.md @@ -4,7 +4,6 @@ - hideFromAPIDocumentation - overwriting with --force - binary responses - - troubleshooting: --verbose - plugin api: responses - description, $stage property - --env - Use database transactions and `create()` when instantiating factory models @@ -17,9 +16,6 @@ - Command scribe:strategy: It would be nice if we had a make strategy command that can help people generate custom strategies - Possible feature: https://github.com/mpociot/laravel-apidoc-generator/issues/731 -# Improvements -- Improve error messaging: there's lots of places where it can crash because of wrong user input. We can try to have more descriptive error messages. - # Tests - Add tests that verify the overwriting behaviour of the command when --force is used