mirror of
https://github.com/ambieco/scribe.git
synced 2026-03-29 06:07:50 +08:00
Improve error handling
This commit is contained in:
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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() ?: ''),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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', []);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
/**
|
||||
|
||||
68
src/Tools/ConsoleOutputUtils.php
Normal file
68
src/Tools/ConsoleOutputUtils.php
Normal 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";
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
@@ -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).'.');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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']),
|
||||
];
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
4
todo.md
4
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user