作者 lackoxygen

add all

/vendor
/.idea
/.vscode
.env
.phpunit.result.cache
composer.lock
\ No newline at end of file
... ...
{
"name": "lackoxygen/showdoc-generation",
"type": "library",
"autoload": {
"psr-4": {
"Lackoxygen\\ShowDocGeneration\\": "src/"
}
},
"authors": [
{
"name": "lackoxygen",
"email": "jingze666@gmail.com"
}
],
"extra": {
"laravel": {
"providers": [
"Lackoxygen\\ShowDocGeneration\\ShowDocGenerationServiceProvider"
]
}
},
"require": {
"doctrine/annotations": "1.14.3"
}
}
... ...
<?php
return [
'api_domain' => '',
'api_key' => '',
'api_token' => ''
];
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
abstract class Annotation
{
public function __construct(array $values)
{
foreach ($this->getSelfProperties() as $property) {
if (isset($values[$property->getName()])) {
$this->{$property->getName()} = $values[$property->getName()];
}
}
}
/**
* @return mixed|\ReflectionProperty[]
*/
protected function getSelfProperties()
{
static $properties;
if (is_null($properties)) {
$ref = new \ReflectionClass($this);
$properties = $ref->getProperties();
}
return $properties;
}
/**
* @return array
*/
public function toArray(): array
{
$array = [];
foreach ($this->getSelfProperties() as $property) {
$array[$property->getName()] = $this->{$property->getName()};
}
return $array;
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({})
*/
final class Catalog extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $value = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("value", type = "string"),
* })
*/
final class Description extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $value = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("key", type = "string"),
@Attribute("remark", type = "string"),
* })
*/
final class HeaderLine extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $key = '';
/**
* @Required()
* @var string
*/
protected string $remark = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Enum;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("method", type = "string"),
* })
*/
final class Method extends Annotation
{
public const METHOD_GET = 'GET';
public const METHOD_POST = 'POST';
public const METHOD_PUT = 'PUT';
public const METHOD_PATCH = 'PATCH';
public const METHOD_DELETE = 'DELETE';
/**
* @Required()
* @Enum({"GET", "POST", "PUT", "PATCH", "DELETE"})
* @var string
*/
protected string $method = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("key", type = "string"),
@Attribute("must", type = "bool"),
@Attribute("type", type = "string"),
@Attribute("remark", type = "string"),
* })
*/
final class ParamLine extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $key = '';
/**
* @var bool
*/
protected bool $must = false;
/**
* @Required()
* @var string
*/
protected string $type = '';
/**
* @Required()
* @var string
*/
protected string $remark = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({})
*/
final class Remark extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $value = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("value", type = "string"),
* })
*/
final class Resp extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $value = '{}';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
use Doctrine\Common\Annotations\Annotation\Required;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("key", type = "string"),
@Attribute("type", type = "string"),
@Attribute("remark", type = "string"),
* })
*/
class RespLine extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $key = '';
/**
* @Required()
* @var string
*/
protected string $type = '';
/**
* @Required()
* @var string
*/
protected string $remark = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("value", type = "string"),
* })
*/
final class Title extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $value = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Annotations;
/**
* @Annotation
* @Target({"METHOD"})
* @Attributes({
@Attribute("path", type = "string"),
* })
*/
final class Url extends Annotation
{
/**
* @Required()
* @var string
*/
protected string $path = '';
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Commands;
use Illuminate\Console\Command;
use Lackoxygen\ShowDocGeneration\Logger;
use Lackoxygen\ShowDocGeneration\Parser\Paster;
use Lackoxygen\ShowDocGeneration\Writer\Writer;
class GenerationCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'doc {--v : print info}
{--prefix=}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'generate documentation';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
$v = $this->input->getOption('v');
Logger::setMode($v ? Logger::DISPLAY : Logger::NONE);
Logger::setOutput($this->output);
Logger::writeln("input option v={$v}");
$prefix = $this->input->getOption('prefix') ?? 'api';
Logger::writeln("input option prefix={$prefix}");
Logger::writeln('parse start');
$cos = Paster::resolve($this->laravel, $prefix);
Logger::writeln('parse end');
Logger::writeln('write start');
$writer = new Writer();
$writer->puts($cos);
Logger::writeln('write end');
return 0;
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration;
use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\OutputInterface;
class Logger
{
public const NONE = 0;
public const DISPLAY = 1;
/**
* @var int
*/
private static int $mode;
/**
* @var OutputInterface $outputInterface
*/
private static OutputInterface $outputInterface;
/**
* @return void
*/
public static function setMode(int $mode)
{
self::$mode = $mode;
}
public static function setOutput(OutputInterface $output)
{
self::$outputInterface = $output;
}
public static function writeln(string $message)
{
if (self::$mode) {
self::$outputInterface->writeln(sprintf("[%s]: %s", now()->toString(), $message));
}
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Parser;
class Doc
{
public array $catalog = [];
public array $title = [];
public array $method = [];
public array $url = [];
public array $headerLine = [];
public array $paramLine = [];
public array $resp = [];
public array $respLine = [];
public array $remark = [];
public array $middles = [];
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Parser;
use Illuminate\Foundation\Application;
use Illuminate\Routing\Route;
use Illuminate\Routing\RouteCollection;
use Illuminate\Routing\Router;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use Lackoxygen\ShowDocGeneration\Annotations\Annotation;
use Lackoxygen\ShowDocGeneration\Annotations\Catalog;
use Lackoxygen\ShowDocGeneration\Annotations\HeaderLine;
use Lackoxygen\ShowDocGeneration\Annotations\Method;
use Lackoxygen\ShowDocGeneration\Annotations\ParamLine;
use Lackoxygen\ShowDocGeneration\Annotations\Remark;
use Lackoxygen\ShowDocGeneration\Annotations\Resp;
use Lackoxygen\ShowDocGeneration\Annotations\RespLine;
use Lackoxygen\ShowDocGeneration\Annotations\Title;
use Lackoxygen\ShowDocGeneration\Annotations\Url;
use Lackoxygen\ShowDocGeneration\Logger;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class Paster
{
/**
* @var array
*/
protected array $annotations = [
Catalog::class,
Title::class,
Url::class,
Method::class,
HeaderLine::class,
ParamLine::class,
Resp::class,
RespLine::class,
Remark::class
];
/**
* @var Application
*/
protected Application $application;
/**
* @var string
*/
protected string $prefix;
/**
* @var Collection|array
*/
protected Collection $docsCollect;
public function __construct()
{
$this->docsCollect = collect();
}
/**
* @param Application $application
* @param string|null $prefix
* @return array|Collection
*/
public static function resolve(Application $application, string $prefix = null)
{
$static = new static();
$static->application = $application;
$static->prefix = $prefix;
try {
$routes = $static->getRoutes();
$static->eachRoutes($routes, function (self $self, Route $route) {
Logger::writeln('load ' . join('|', $route->methods()) . ' ' . $route->uri());
try {
$doc = $self->resolveAnnotation($route);
$doc->middles = $route->middleware();
$self->docsCollect->push($doc);
} catch (\ReflectionException $e) {
Logger::writeln('error ' . $e->getMessage());
}
});
} catch (NotFoundExceptionInterface|ContainerExceptionInterface $e) {
Logger::writeln('error ' . $e->getMessage());
}
return $static->docsCollect;
}
/**
* @param RouteCollection $routes
* @param $callback
* @return void
*/
protected function eachRoutes(RouteCollection $routes, $callback)
{
/**
* @var Route $route
*/
foreach ($routes as $route) {
if ($this->isMatch($route)) {
$callback($this, $route);
}
}
}
/**
* @param Route $route
* @return bool
*/
protected function isMatch(Route $route): bool
{
if ($this->prefix && $prefixArray = explode(',', $this->prefix)) {
if (!in_array($route->getPrefix(), $prefixArray)) {
return false;
}
}
$action = $route->getAction();
if (!isset($action['controller'])) {
return false;
}
return true;
}
/**
* @param Route $route
* @return Doc
* @throws \ReflectionException
*/
protected function resolveAnnotation(Route $route): Doc
{
Logger::writeln('parse ' . join('|', $route->methods()) . ' ' . $route->uri() . ' Annotation');
$reader = new \Doctrine\Common\Annotations\AnnotationReader();
$refClass = new \ReflectionClass(($route->getController()));
$method = $refClass->getMethod($route->getActionMethod());
$doc = new Doc();
foreach ($this->annotations as $annot) {
Logger::writeln('parse ' . join('|', $route->methods()) . ' ' . $route->uri() . ' method annotation ' . $annot);
$annotation = $reader->getMethodAnnotation($method, $annot);
$pro = Str::camel(class_basename($annot));
if (!$annotation instanceof Annotation) {
continue;
}
if (Str::endsWith($annot, 'Line')) {
$doc->{$pro}[] = $annotation->toArray();
} else {
$doc->{$pro} = $annotation->toArray();
}
}
return $doc;
}
/**
* @return \Illuminate\Routing\RouteCollectionInterface|RouteCollection
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
protected function getRoutes()
{
/**
* @var Router $router
*/
$router = $this->application->get('router');
return $router->getRoutes();
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration;
use Illuminate\Support\ServiceProvider;
use Lackoxygen\ShowDocGeneration\Commands\GenerationCommand;
class ShowDocGenerationServiceProvider extends ServiceProvider
{
public function boot()
{
if ($this->app->runningInConsole()) {
$configPath = __DIR__ . '/../publish/doc.php';
$this->publishes([
$configPath => config_path('doc.php')
], 'lackoxygen-doc');
}
}
public function register()
{
$this->commands([
GenerationCommand::class
]);
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Writer;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\RequestOptions;
use Illuminate\Support\Arr;
use Lackoxygen\ShowDocGeneration\Logger;
use Lackoxygen\ShowDocGeneration\Parser\Doc;
class Client
{
protected \GuzzleHttp\Client $engine;
/**
* @var array
*/
protected array $config;
public function __construct()
{
Logger::writeln('new GuzzleHttp\\Client');
$this->engine = new \GuzzleHttp\Client([
RequestOptions::TIMEOUT => 5.00
]);
$this->config = (array)config('doc', []);
Logger::writeln('read doc config ' . \json_encode($this->config));
}
/**
* @param Doc $doc
* @return void
*/
public function send(Doc $doc)
{
try {
$params = [
'api_key' => (string)Arr::get($this->config, 'api_key'),
'api_token' => (string)Arr::get($this->config, 'api_token'),
'cat_name' => Arr::get($doc->catalog, 'value', ''),
'page_title' => Arr::get($doc->title, 'value', ''),
'page_content' => (new Markdown($doc))->toString(),
's_number' => 99
];
Logger::writeln('guzzle form params ' . json_encode($params, JSON_UNESCAPED_UNICODE));
$ret = $this->engine->post((string)Arr::get($this->config, 'api_url'),
[
RequestOptions::FORM_PARAMS => $params
]
);
Logger::writeln('guzzle status ' . $ret->getStatusCode());
Logger::writeln('guzzle response ' . \json_encode(\json_decode($ret->getBody()->getContents()), JSON_UNESCAPED_UNICODE));
} catch (GuzzleException $e) {
Logger::writeln('guzzle error .' . $e->getMessage());
}
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Writer;
use Illuminate\Support\Arr;
use Lackoxygen\ShowDocGeneration\Annotations\Method;
use Lackoxygen\ShowDocGeneration\Parser\Doc;
class Markdown
{
protected string $template = <<<t
##### 简要描述
- {{title}}
##### 请求URL
- ` {{url}} `
##### 请求方式
- {{method}}
##### 参数
|参数名|必选|类型|说明|
|:----|:---|:----- |-----|
{{params}}
##### 中间件
{{middles}}
##### 请求头
|参数名|说明|
|:---- |:---|
{{headers}}
##### 返回示例
```
{{resp}}
```
##### 返回参数说明
|参数名|类型|说明|
|:-----|:-----|:-----------------------------|
{{respRemark}}
##### 备注
- 最后更新时间:{{updateTime}}
t;
protected string $output = '';
public function __construct(Doc $doc)
{
$editTemplate = $this->template;
$replaces = [
'title' => Arr::get($doc->title, 'value'),
'url' => Arr::get($doc->url, 'path'),
'method' => Arr::get($doc->method, 'method', Method::METHOD_GET),
'headers' => join("\n", $this->tableTbody($doc->headerLine)),
'params' => join("\n", $this->tableTbody($doc->paramLine)),
'resp' => Arr::get($doc->resp, 'value', '{}'),
'respRemark' => join("\n", $this->tableTbody($doc->respLine)),
'middles' => join("\n", $this->unorderedList($doc->middles)),
'updateTime' => now()->toString()
];
$this->output = \str_replace(
\array_map(function (string $key) {
return '{{' . $key . '}}';
}, \array_keys($replaces)),
\array_values($replaces),
$editTemplate
);
}
protected function unorderedList(array $list): array
{
return \array_map(function ($v) {
return '- ' . $v;
}, $list);
}
protected function tableTbody(array $tbody): array
{
return \array_map(function (array $line) {
$line = \array_map(function ($v) {
if (\is_bool($v)) {
$v = $v ? '是' : '否';
}
return $v;
}, $line);
return '|' . \join('|', \array_values($line)) . '|';
}, $tbody);
}
public function toString(): string
{
return $this->output;
}
}
... ...
<?php
namespace Lackoxygen\ShowDocGeneration\Writer;
use Illuminate\Support\Collection;
use Lackoxygen\ShowDocGeneration\Parser\Doc;
class Writer
{
public function puts(Collection $docs)
{
$client = new Client();
$docs->each(function (Doc $doc) use ($client) {
$client->send($doc);
});
}
}
... ...