swagger-php/tools/src/Docs/RefGenerator.php

335 lines
10 KiB
PHP

<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Tools\Docs;
use OpenApi\Analysers\TokenScanner;
use OpenApi\Annotations\AbstractAnnotation;
class RefGenerator
{
const ATTRIBUTES = 'Attributes';
const ANNOTATIONS = 'Annotations';
const NO_DETAILS_AVAILABLE = 'No details available.';
protected $scanner;
protected $projectRoot;
public function __construct($projectRoot)
{
$this->scanner = new TokenScanner();
$this->projectRoot = realpath($projectRoot);
}
public function docPath(string $relativeName): string
{
return $this->projectRoot . '/docs/' . $relativeName;
}
public function preamble(string $type): string
{
return <<< EOT
# $type
This page is generated automatically from the `swagger-php` sources.
For improvements head over to [GitHub](https://github.com/zircote/swagger-php) and create a PR ;)
In addition to this page, there are also a number of [examples](https://github.com/zircote/swagger-php/tree/master/Examples#readme) which might help you out.
EOT;
}
public function classesForType(string $type): array
{
$classes = [];
$dir = new \DirectoryIterator($this->projectRoot . '/src/' . $type);
foreach ($dir as $entry) {
if (!$entry->isFile() || $entry->getExtension() != 'php') {
continue;
}
$class = $entry->getBasename('.php');
if (in_array($class, ['AbstractAnnotation', 'Operation', 'ParameterTrait', 'OperationTrait'])) {
continue;
}
$classes[$class] = [
'fqdn' => 'OpenApi\\' . $type . '\\' . $class,
'filename' => $entry->getPathname(),
];
}
ksort($classes);
return $classes;
}
public function types(): array
{
return [self::ANNOTATIONS, self::ATTRIBUTES];
}
public function formatHeader(string $name, string $type): string
{
return <<< EOT
## [$name](https://github.com/zircote/swagger-php/tree/master/src/$type/$name.php)
EOT;
}
public function formatAttributesDetails(string $name, string $fqdn, string $filename): string
{
$rctor = (new \ReflectionClass($fqdn))->getMethod('__construct');
ob_start();
$rc = new \ReflectionClass($fqdn);
$classDocumentation = $this->extractDocumentation($rc->getDocComment());
echo $classDocumentation['content'] . PHP_EOL;
$ctorDocumentation = $this->extractDocumentation($rc->getMethod('__construct')->getDocComment());
$params = $ctorDocumentation['params'];
$this->treeDetails($fqdn);
$parameters = $rctor->getParameters();
if ($parameters) {
echo PHP_EOL . '#### Parameters' . PHP_EOL;
echo '---' . PHP_EOL;
echo '<dl>' . PHP_EOL;
foreach ($parameters as $rp) {
$parameter = $rp->getName();
$def = array_key_exists($parameter, $params)
? $params[$parameter]
: '';
if ($var = $this->getReflectionType($fqdn, $rp, true, $def)) {
$var = ' : <span style="font-family: monospace;">' . $var . '</span>';
}
echo ' <dt><strong>' . $parameter . '</strong>' . $var . '</dt>' . PHP_EOL;
echo ' <dd>';
echo '<p>' . self::NO_DETAILS_AVAILABLE . '</p>';
echo '</dd>' . PHP_EOL;
}
echo '</dl>' . PHP_EOL;
}
if ($classDocumentation['see']) {
echo PHP_EOL . '#### Reference' . PHP_EOL;
echo '---' . PHP_EOL;
foreach ($classDocumentation['see'] as $link) {
echo '- ' . $link . PHP_EOL;
}
}
echo PHP_EOL;
return ob_get_clean();
}
/**
* @param class-string<AbstractAnnotation> $fqdn
*/
public function formatAnnotationsDetails(string $name, string $fqdn, string $filename): string
{
$details = $this->scanner->scanFile($filename);
ob_start();
$rc = new \ReflectionClass($fqdn);
$classDocumentation = $this->extractDocumentation($rc->getDocComment());
echo $classDocumentation['content'] . PHP_EOL;
$this->treeDetails($fqdn);
$nestedProps = $this->getNestedProperties($fqdn);
$properties = array_filter($details[$fqdn]['properties'], function ($property) use ($fqdn, $nestedProps) {
return !in_array($property, $fqdn::$_blacklist) && $property[0] != '_' && !in_array($property, $nestedProps);
});
if ($properties) {
echo PHP_EOL . '#### Properties' . PHP_EOL;
echo '---' . PHP_EOL;
echo '<dl>' . PHP_EOL;
foreach ($properties as $property) {
$rp = new \ReflectionProperty($fqdn, $property);
$propertyDocumentation = $this->extractDocumentation($rp->getDocComment());
if ($var = $this->getReflectionType($fqdn, $rp, false, $propertyDocumentation['var'])) {
$var = ' : <span style="font-family: monospace;">' . $var . '</span>';
}
echo ' <dt><strong>' . $property . '</strong>' . $var . '</dt>' . PHP_EOL;
echo ' <dd>';
echo '<p>' . nl2br($propertyDocumentation['content'] ?: self::NO_DETAILS_AVAILABLE) . '</p>';
if ($propertyDocumentation['see']) {
$links = [];
foreach ($propertyDocumentation['see'] as $see) {
if ($link = $this->linkFromMarkup($see)) {
$links[] = $link;
}
}
if ($links) {
echo '<p><i>See</i>: ' . implode(', ', $links) . '</p>';
}
}
echo '</dd>' . PHP_EOL;
}
echo '</dl>' . PHP_EOL;
}
if ($classDocumentation['see']) {
echo PHP_EOL . '#### Reference' . PHP_EOL;
echo '---' . PHP_EOL;
foreach ($classDocumentation['see'] as $link) {
echo '- ' . $link . PHP_EOL;
}
}
echo PHP_EOL;
return ob_get_clean();
}
/**
* @param class-string<AbstractAnnotation> $fqdn
*/
protected function getNestedProperties($fqdn): array
{
$props = [];
foreach ($fqdn::$_nested as $details) {
$props[] = ((array) $details)[0];
}
return $props;
}
/**
* @param class-string<AbstractAnnotation> $fqdn
*/
protected function treeDetails($fqdn)
{
if ($fqdn::$_parents) {
echo PHP_EOL . '#### Allowed in' . PHP_EOL;
echo '---' . PHP_EOL;
$parents = array_map(function (string $parent) {
$shortName = $this->shortName($parent);
return '<a href="#' . strtolower($shortName) . '">' . $shortName . '</a>';
}, $fqdn::$_parents);
echo implode(', ', $parents) . PHP_EOL;
}
if ($fqdn::$_nested) {
echo PHP_EOL . '#### Nested elements' . PHP_EOL;
echo '---' . PHP_EOL;
$nested = array_map(function (string $nested) {
$shortName = $this->shortName($nested);
return '<a href="#' . strtolower($shortName) . '">' . $shortName . '</a>';
}, array_keys($fqdn::$_nested));
echo implode(', ', $nested) . PHP_EOL;
}
}
protected function shortName(string $class): string
{
return str_replace(['OpenApi\\Annotations\\', 'OpenApi\\Attributes\\'], '', $class);
}
protected function linkFromMarkup(string $see): ?string
{
preg_match('/\[([^]]+)]\((.*)\)/', $see, $matches);
return 3 == count($matches) ? '<a href="' . $matches[2] . '">' . $matches[1] . '</a>' : null;
}
protected function getReflectionType(string $fqdn, $rp, bool $preferDefault = false, string $def = ''): string
{
$var = [];
if ($type = $rp->getType()) {
if ($type instanceof \ReflectionUnionType) {
foreach ($type->getTypes() as $type) {
$var[] = $type->getName();
}
} else {
$var[] = $type->getName();
}
if ($type->allowsNull()) {
$var[] = 'null';
}
}
if ($def && (!$var || $preferDefault)) {
if ($preferDefault) {
$var = [];
}
$var = array_merge($var, explode('|', $def));
}
return implode('|', array_map(function ($item) {
return htmlentities($item);
}, array_unique($var)));
}
protected function extractDocumentation($docblock): array
{
if (!$docblock) {
return ['content' => '', 'see' => [], 'var' => '', 'params' => []];
}
$comment = preg_split('/(\n|\r\n)/', (string) $docblock);
$comment[0] = preg_replace('/[ \t]*\\/\*\*/', '', $comment[0]); // strip '/**'
$i = count($comment) - 1;
$comment[$i] = preg_replace('/\*\/[ \t]*$/', '', $comment[$i]); // strip '*/'
$see = [];
$var = '';
$params = [];
$contentLines = [];
$append = false;
foreach ($comment as $line) {
$line = ltrim($line, "\t *");
if (substr($line, 0, 1) === '@') {
if (substr($line, 0, 5) === '@see ') {
$see[] = trim(substr($line, 5));
}
if (substr($line, 0, 5) === '@var ') {
$var = trim(substr($line, 5));
}
if (substr($line, 0, 7) === '@param ') {
preg_match('/^([^\$]+)\$(.+)$/', trim(substr($line, 7)), $match);
if (3 == count($match)) {
$params[trim($match[2])] = trim($match[1]);
}
}
continue;
}
if ($append) {
$i = count($contentLines) - 1;
$contentLines[$i] = substr($contentLines[$i], 0, -1) . $line;
} else {
$contentLines[] = $line;
}
$append = (substr($line, -1) === '\\');
}
$content = trim(implode("\n", $contentLines));
return ['content' => $content, 'see' => $see, 'var' => $var, 'params' => $params];
}
}