diff --git a/.markdownlint.yaml b/.markdownlint.yaml index 428d05582..a4597bde4 100644 --- a/.markdownlint.yaml +++ b/.markdownlint.yaml @@ -258,4 +258,5 @@ MD053: "include:file", "include:package-list", "include:template", + "include:docblock", ] diff --git a/composer.json b/composer.json index 43a5ba6ce..8464a5321 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "league/commonmark": "^2.4", "nuwave/lighthouse": "^6.5.0", "opis/json-schema": "^2.3.0", + "phpdocumentor/reflection-docblock": "^5.2", "phpdocumentor/type-resolver": "^1.7", "phpstan/phpdoc-parser": "^1.23", "psr/http-message": "^1.0.0|^2.0.0", diff --git a/packages/documentator/composer.json b/packages/documentator/composer.json index 6f3169009..a2f197019 100644 --- a/packages/documentator/composer.json +++ b/packages/documentator/composer.json @@ -23,6 +23,7 @@ "composer/semver": "^3.2", "laravel/framework": "^9.21.0|^10.0.0", "league/commonmark": "^2.4", + "phpdocumentor/reflection-docblock": "^5.2", "symfony/filesystem": "^6.3.0", "symfony/finder": "^6.3.0", "symfony/process": "^6.3.0", diff --git a/packages/documentator/docs/Commands/preprocess.md b/packages/documentator/docs/Commands/preprocess.md index 140c145b8..11c488afb 100644 --- a/packages/documentator/docs/Commands/preprocess.md +++ b/packages/documentator/docs/Commands/preprocess.md @@ -29,6 +29,17 @@ Where: ## Instructions +### `[include:docblock]: ` + +* `` - File path. +* `` - additional parameters + * `summary` - Include the class summary? (default `false`) + * `description` - Include the class description? (default `true`) + +Includes the docblock of the first PHP class/interface/trait/enum/etc +from `` file. Inline tags include as is. Other tags are +ignored. + ### `[include:document-list]: ` * `` - Directory path. diff --git a/packages/documentator/src/Preprocessor/Exceptions/TargetIsNotValidPhpFile.php b/packages/documentator/src/Preprocessor/Exceptions/TargetIsNotValidPhpFile.php new file mode 100644 index 000000000..6c0b7336b --- /dev/null +++ b/packages/documentator/src/Preprocessor/Exceptions/TargetIsNotValidPhpFile.php @@ -0,0 +1,22 @@ + + */ +class IncludeDocBlock implements ParameterizableInstruction { + public function __construct( + protected readonly PackageViewer $viewer, + ) { + // empty + } + + #[Override] + public static function getName(): string { + return 'include:docblock'; + } + + #[Override] + public static function getDescription(): string { + return <<<'DESC' + Includes the docblock of the first PHP class/interface/trait/enum/etc + from `` file. Inline tags include as is. Other tags are + ignored. + DESC; + } + + #[Override] + public static function getTargetDescription(): ?string { + return 'File path.'; + } + + #[Override] + public static function getParameters(): string { + return IncludeDocBlockParameters::class; + } + + /** + * @inheritDoc + */ + #[Override] + public static function getParametersDescription(): array { + return [ + 'summary' => 'Include the class summary? (default `false`)', + 'description' => 'Include the class description? (default `true`)', + ]; + } + + #[Override] + public function process(string $path, string $target, Serializable $parameters): string { + // File? + $file = Path::getPath(dirname($path), $target); + $content = file_get_contents($file); + + if ($content === false) { + throw new TargetIsNotFile($path, $target); + } + + // Class? + try { + $class = null; + $parser = (new ParserFactory())->create(ParserFactory::PREFER_PHP7); + $stmts = (array) $parser->parse($content); + $finder = new NodeFinder(); + $class = $finder->findFirst($stmts, static function (Node $node): bool { + return $node instanceof ClassLike; + }); + } catch (Exception $exception) { + throw new TargetIsNotValidPhpFile($path, $target, $exception); + } + + if (!($class instanceof ClassLike)) { + return ''; + } + + // DocBlock? + $comment = $class->getDocComment()?->getText(); + + if (!$comment) { + return ''; + } + + // Parse + $eol = "\n"; + $result = ''; + $factory = DocBlockFactory::createInstance(); + $docblock = $factory->create($comment); + + if ($parameters->summary) { + $summary = trim($docblock->getSummary()); + + if ($summary) { + $result .= $summary.$eol.$eol; + } + } + + if ($parameters->description) { + $description = trim((string) $docblock->getDescription()); + + if ($description) { + $result .= $description.$eol.$eol; + } + } + + if ($result) { + $result = trim($result).$eol; + } + + // Return + return $result; + } +} diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockParameters.php b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockParameters.php new file mode 100644 index 000000000..654ffea60 --- /dev/null +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockParameters.php @@ -0,0 +1,14 @@ + + // ========================================================================= + /** + * @dataProvider dataProviderProcess + */ + public function testProcess(Exception|string $expected, string $file, IncludeDocBlockParameters $params): void { + $file = self::getTestData()->file($file); + $instance = Container::getInstance()->make(IncludeDocBlock::class); + + if ($expected instanceof Exception) { + self::expectExceptionObject($expected); + } else { + $expected = self::getTestData()->content($expected); + } + + self::assertEquals($expected, $instance->process($file->getPathname(), $file->getFilename(), $params)); + } + + public function testProcessAbsolute(): void { + $path = 'invalid/directory'; + $file = self::getTestData()->path('Valid.txt'); + $params = new IncludeDocBlockParameters(); + $instance = Container::getInstance()->make(IncludeDocBlock::class); + $expected = self::getTestData()->content('ValidExpected.txt'); + + self::assertEquals($expected, $instance->process($path, $file, $params)); + } + // + + // + // ========================================================================= + /** + * @return array + */ + public static function dataProviderProcess(): array { + return [ + 'default' => [ + 'ValidExpected.txt', + 'Valid.txt', + new IncludeDocBlockParameters(), + ], + 'with summary' => [ + 'ValidWithSummaryExpected.txt', + 'Valid.txt', + new IncludeDocBlockParameters(summary: true), + ], + 'only summary' => [ + 'ValidOnlySummaryExpected.txt', + 'Valid.txt', + new IncludeDocBlockParameters(summary: true, description: false), + ], + 'no docblock' => [ + 'NoDocblockExpected.txt', + 'NoDocblock.txt', + new IncludeDocBlockParameters(), + ], + 'invalid' => [ + new TargetIsNotValidPhpFile(__DIR__.'/IncludeDocBlockTest/Invalid.txt', 'Invalid.txt'), + 'Invalid.txt', + new IncludeDocBlockParameters(), + ], + ]; + } + // +} diff --git a/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockTest/Invalid.txt b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockTest/Invalid.txt new file mode 100644 index 000000000..b7f357f04 --- /dev/null +++ b/packages/documentator/src/Preprocessor/Instructions/IncludeDocBlockTest/Invalid.txt @@ -0,0 +1,10 @@ +addInstruction(IncludeExec::class); $this->addInstruction(IncludeExample::class); $this->addInstruction(IncludeTemplate::class); + $this->addInstruction(IncludeDocBlock::class); $this->addInstruction(IncludePackageList::class); $this->addInstruction(IncludeDocumentList::class); }