Je parse, vue que c'est un cas basique et que je n'ai pas pu faire marcher la Javadoc là dessus: ce ne sont pas des définitions de fonctions/classes. Le pattern est
Et habituellement, j'appelle "forEach" sur cette NodeList (parfois sur la même ligne, parfois à la ligne suivante... d'où le fait que je tronque le pattern à ce niveau là). Cela reste à peu près la même syntaxe, mais comme le querySelectorAll n'est pas une définition de fonction, je ne voyais pas comment intégrer ça à de la javadoc existante. Et je restreint uniquement aux fichiers JS des templates (si, pour un endpoint spécifique, j'ai ce pattern, alors je m'en fous un peu: c'est spécifique à une seule page, donc quand je travaille sur les autres, soit 90% du temps vu que j'ai plus d'une dizaine de pages, cela ne m'intéresse pas).
Du coup, le parsing tiens en 10 lignes grosso modo:
Où HtmlDocInfos est un simple bean; cette liste de beans est formattée en HTML en dessous.
Pour la construction de la doc "REST API", c'est un peu plus lourd: je vais parser le contenu des classes des endpoints, et j'en tire les informations correspondantes. J'avais vu, quand j'avais attaqué ce morceau-là pour le fun, qu'il existait des parser qui se basent sur la PHPDoc de la classe. Mais j'avais peur que cette PHPDoc ne soit alors jamais en phase avec le contenu du code du endpoint, d'où le parser de code.
Mais dans ce cas-là, ça montre clairement ses limites: je n'arrive pas à descendre aisément jusqu'au caractère "required/optional" des paramètres, ou à déterminer les réponses (codes HTTP et format) retournables par le endpoint... Je changerai peut-être de stratégie, et je passerai finalement par la PHPDoc, en rajoutant quelques inspections dans mon plugin IntelliJ pour s'assurer que la doc et le code soient en phase.
Actuellement, ça tiens en ça:
Code :
(() => {
...
\t/**
\t * Documentation (multiligne si besoin, avec HTML si besoin)
\t */
\tdocument.querySelectorAll('<selector>')
Et habituellement, j'appelle "forEach" sur cette NodeList (parfois sur la même ligne, parfois à la ligne suivante... d'où le fait que je tronque le pattern à ce niveau là). Cela reste à peu près la même syntaxe, mais comme le querySelectorAll n'est pas une définition de fonction, je ne voyais pas comment intégrer ça à de la javadoc existante. Et je restreint uniquement aux fichiers JS des templates (si, pour un endpoint spécifique, j'ai ce pattern, alors je m'en fous un peu: c'est spécifique à une seule page, donc quand je travaille sur les autres, soit 90% du temps vu que j'ai plus d'une dizaine de pages, cela ne m'intéresse pas).
Du coup, le parsing tiens en 10 lignes grosso modo:
/**
* @return HtmlDocInfos[]
*/
private function buildHtmlDoc(): array {
$htmlDocs = array();
foreach ($this->jsFiles as $jsFile) {
$jsPath = realpath($jsFile);
$jsContent = file_get_contents($jsPath);
if (!preg_match_all(
'~(?:/\\*\\*' .
'((?:\\n\\s+\\*\\s+(?:.*))+)' .
'\\n\\s*\\*/)?' .
'\\n\\tdocument\\.querySelectorAll\\(\'(.+)\'\\)~m',
$jsContent,
$docs,
PREG_SET_ORDER)) {
continue;
}
foreach ($docs as $doc) {
$htmlDoc = new HtmlDocInfos();
$htmlDoc->file = $this->htmlDocUrlForPath($jsFile);
$htmlDoc->documentation = trim(
preg_replace_callback(
'~\\n\\s+\\* (.)~',
static function (array $match): string {
return (strtoupper($match[1]) === $match[1] ? "\n" : " ") . $match[1];
},
$doc[1] ?? ''));
$htmlDoc->selector = $doc[2];
$htmlDocs[] = $htmlDoc;
}
}
return $htmlDocs;
}
Où HtmlDocInfos est un simple bean; cette liste de beans est formattée en HTML en dessous.
Pour la construction de la doc "REST API", c'est un peu plus lourd: je vais parser le contenu des classes des endpoints, et j'en tire les informations correspondantes. J'avais vu, quand j'avais attaqué ce morceau-là pour le fun, qu'il existait des parser qui se basent sur la PHPDoc de la classe. Mais j'avais peur que cette PHPDoc ne soit alors jamais en phase avec le contenu du code du endpoint, d'où le parser de code.
Mais dans ce cas-là, ça montre clairement ses limites: je n'arrive pas à descendre aisément jusqu'au caractère "required/optional" des paramètres, ou à déterminer les réponses (codes HTTP et format) retournables par le endpoint... Je changerai peut-être de stratégie, et je passerai finalement par la PHPDoc, en rajoutant quelques inspections dans mon plugin IntelliJ pour s'assurer que la doc et le code soient en phase.
Actuellement, ça tiens en ça:
/**
* @return object
* @throws ReflectionException
*/
private function getOpenApiPaths(): object {
$paths = array();
$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator(
$this->endpointsDirectory,
RecursiveDirectoryIterator::SKIP_DOTS
| RecursiveDirectoryIterator::CURRENT_AS_PATHNAME
| RecursiveDirectoryIterator::UNIX_PATHS),
RecursiveIteratorIterator::LEAVES_ONLY);
foreach ($iterator as $it) {
if (strpos($it, 'Endpoint.php') === false) {
// Ignore non endpoint files
continue;
}
$endpointRelativePath = substr($it, strlen($this->endpointsDirectory), -4);
$endpointClass = $this->endpointBaseClass . str_replace('/', '\\', $endpointRelativePath);
$reflection = new ReflectionClass($endpointClass);
if ($reflection->isAbstract() || $reflection->isInterface()) {
// Don't treat interfaces or abstract classes, since they are utility classes
continue;
}
$endpointConstructor = $reflection->getConstructor();
if ($endpointConstructor !== null && count($endpointConstructor->getParameters()) > 0) {
// Endpoint requires a parameter: it cannot be a web endpoint (it's more like a CLI one) so ignore
continue;
}
$endpoint = new $endpointClass();
$skipEndpoint = $this->doSkipEndpoint($endpoint);
if ($skipEndpoint === true) {
continue;
} else if ($skipEndpoint === null) {
// Unexpected class!
throw new LogicException("The endpoint $endpointClass does not extend a known abstract class");
}
$path = array();
$classDoc = $this->cleanDocCommentLines($reflection->getDocComment());
$path['summary'] = $classDoc
? $classDoc[0]
: 'Not documented';
$path['description'] = $classDoc && count($classDoc) > 1
? implode("\n", array_slice($classDoc, 1))
: 'Not documented';
$parameters = array();
foreach ($reflection->getReflectionConstants() as $const) {
if (strpos($const->getName(), 'PARAM_') === 0) {
$parameters[] = (object)array(
'name' => $const->getValue(),
'in' => 'query',
'description' => implode("\n", $this->cleanDocCommentLines($const->getDocComment())) ?: 'Not documented'
/*'required' => true*/
);
}
}
$responses = array();
// $operation['responses'][200] = (object)array(
// 'description' => 'Not documented',
// 'content' => array(
// 'text/html' => (object)array(),
// 'application/json' => (object)array()));
$operation = array(
'operationId' => basename($it, '.php'),
'parameters' => $parameters,
'responses' => (object)$responses);
$verb = $this->getEndpointVerb($endpoint);
if ($verb === null) {
throw new LogicException("The web endpoint $endpointClass has unknown HTTP verb");
}
$path[$verb] = (object)$operation;
$paths['/' . dirname($endpointRelativePath) . '/'] = (object)$path;
}
return (object)$paths;
}