Improve error handling

This commit is contained in:
shalvah
2020-05-08 20:29:32 +01:00
parent 7d46bcbcdd
commit fc5d112362
22 changed files with 213 additions and 209 deletions

View File

@@ -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');

View File

@@ -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);

View File

@@ -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() ?: ''),

View File

@@ -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 <code>Africa/Accra</code>. ";
$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;

View File

@@ -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();

View File

@@ -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 {

View File

@@ -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) {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -1,16 +0,0 @@
<?php
namespace Knuckles\Scribe;
class Scribe
{
/**
* Get the middleware for Laravel routes.
*
* @return array
*/
protected static function middleware()
{
return config('scribe.laravel.middleware', []);
}
}

View File

@@ -2,19 +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 AnnotationParser
{
/**

View File

@@ -0,0 +1,68 @@
<?php
namespace Knuckles\Scribe\Tools;
use Illuminate\Routing\Route;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
class ConsoleOutputUtils
{
/**
* @var \Shalvah\Clara\Clara
*/
private static $clara;
public static function bootstrapOutput(OutputInterface $outputInterface)
{
$showDebug = Flags::$shouldBeVerbose;
self::$clara = clara('knuckleswtf/scribe', $showDebug)
->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";
}
}

View File

@@ -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));

View File

@@ -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).'.');
}
/**

View File

@@ -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

View File

@@ -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']),
];

View File

@@ -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;
}
}

View File

@@ -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');

View File

@@ -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 <code>Africa/Accra</code>.',
'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 <code>3</code>, <code>5</code>, or <code>6</code>.',
'type' => 'string',
],
],

View File

@@ -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;

View File

@@ -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)

View File

@@ -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