Skip to content

Commit

Permalink
feat(documentator): Fetching instruction parameters via Reflection AP…
Browse files Browse the repository at this point in the history
…I/PhpDoc (#149).
  • Loading branch information
LastDragon-ru authored Mar 27, 2024
2 parents bdb4afb + b66d8e4 commit 639792b
Show file tree
Hide file tree
Showing 14 changed files with 325 additions and 93 deletions.
12 changes: 6 additions & 6 deletions packages/documentator/docs/Commands/preprocess.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Where:

* `<target>` - File path.
* `<parameters>` - additional parameters
* `summary` - Include the class summary? (default `true`)
* `description` - Include the class description? (default `true`)
* `summary: bool = true` - Include the class summary?
* `description: bool = true` - Include the class description?

Includes the docblock of the first PHP class/interface/trait/enum/etc
from `<target>` file. Inline tags include as is except `@see`/`@link`
Expand All @@ -44,8 +44,8 @@ which will be replaced to FQCN (if possible). Other tags are ignored.

* `<target>` - Directory path.
* `<parameters>` - additional parameters
* `depth` - Default is `0` (no nested directories). The `null` removes limits.
* `template` - Blade template
* `depth: array|string|int|null = 0` - [Directory Depth](https://symfony.com/doc/current/components/finder.html#directory-depth) (eg the `0` means no nested directories, the `null` removes limits).
* `template: string = 'default'` - Blade template.

Returns the list of `*.md` files in the `<target>` directory. Each file
must have `# Header` as the first construction. The first paragraph
Expand Down Expand Up @@ -79,7 +79,7 @@ Includes the `<target>` file.

* `<target>` - Directory path.
* `<parameters>` - additional parameters
* `template` - Blade template
* `template: string = 'default'` - Blade template.

Generates package list from `<target>` directory. The readme file will be
used to determine package name and summary.
Expand All @@ -88,7 +88,7 @@ used to determine package name and summary.

* `<target>` - File path.
* `<parameters>` - additional parameters
* `data` - Array of variables (`${name}`) to replace (required).
* `data: array` - Array of variables (`${name}`) to replace.

Includes the `<target>` as a template.

Expand Down
85 changes: 84 additions & 1 deletion packages/documentator/src/Commands/Preprocess.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,28 @@
use LastDragon_ru\LaraASP\Documentator\Package;
use LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\ParameterizableInstruction;
use LastDragon_ru\LaraASP\Documentator\Preprocessor\Preprocessor;
use LastDragon_ru\LaraASP\Documentator\Utils\PhpDoc;
use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable;
use Override;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;

use function array_map;
use function explode;
use function getcwd;
use function gettype;
use function implode;
use function is_a;
use function is_scalar;
use function ksort;
use function rtrim;
use function str_replace;
use function strtr;
use function trim;
use function var_export;

/**
* @see Preprocessor
Expand Down Expand Up @@ -116,7 +127,7 @@ protected function getInstructionsHelp(Preprocessor $preprocessor): string {
$desc = $instruction::getDescription();
$target = $instruction::getTargetDescription();
$params = is_a($instruction, ParameterizableInstruction::class, true)
? $instruction::getParametersDescription()
? $this->getInstructionParameters($instruction)
: null;

if ($target !== null && $params !== null) {
Expand Down Expand Up @@ -160,4 +171,76 @@ protected function getInstructionsHelp(Preprocessor $preprocessor): string {

return implode("\n\n", $help);
}

/**
* @template T of Serializable
*
* @param class-string<ParameterizableInstruction<T>> $instruction
*
* @return array<string, string>
*/
protected function getInstructionParameters(string $instruction): array {
// Explicit? (deprecated)
$parameters = $instruction::getParametersDescription();

if ($parameters) {
return $parameters;
}

// Nope
$class = new ReflectionClass($instruction::getParameters());
$properties = $class->getProperties(ReflectionProperty::IS_PUBLIC);
$parameters = [];

foreach ($properties as $property) {
// Ignored?
if (!$property->isPublic() || $property->isStatic()) {
continue;
}

// Name
$name = $property->getName();
$definition = $name;
$hasDefault = $property->hasDefaultValue();
$theDefault = $hasDefault
? $property->getDefaultValue()
: null;

if ($property->hasType()) {
$definition = "{$definition}: {$property->getType()}";
}

if ($property->isPromoted()) {
foreach ($class->getConstructor()?->getParameters() ?? [] as $parameter) {
if ($parameter->getName() === $name) {
$hasDefault = $parameter->isDefaultValueAvailable();
$theDefault = $hasDefault
? $parameter->getDefaultValue()
: null;

break;
}
}
}

if ($hasDefault) {
$default = is_scalar($theDefault) ? var_export($theDefault, true) : '<'.gettype($theDefault).'>';
$definition = "{$definition} = {$default}";
} else {
// empty
}

// Description
$doc = new PhpDoc($property->getDocComment() ?: null);
$description = $doc->getSummary() ?: '_No description provided_.';
$description = trim(
implode(' ', array_map(rtrim(...), explode("\n", str_replace("\r\n", "\n", $description)))),
);

// Add
$parameters[$definition] = $description;
}

return $parameters;
}
}
109 changes: 109 additions & 0 deletions packages/documentator/src/Commands/PreprocessTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
<?php declare(strict_types = 1);

namespace LastDragon_ru\LaraASP\Documentator\Commands;

use LastDragon_ru\LaraASP\Documentator\Preprocessor\Contracts\ParameterizableInstruction;
use LastDragon_ru\LaraASP\Documentator\Testing\Package\TestCase;
use LastDragon_ru\LaraASP\Serializer\Contracts\Serializable;
use Override;
use PHPUnit\Framework\Attributes\CoversClass;

/**
* @internal
*/
#[CoversClass(Preprocess::class)]
final class PreprocessTest extends TestCase {
public function testGetInstructionParameters(): void {
$command = new class() extends Preprocess {
/**
* @inheritDoc
*/
#[Override]
public function getInstructionParameters(string $instruction): array {
return parent::getInstructionParameters($instruction);
}
};

self::assertEquals(
[
'publicPropertyWithoutDefaultValue: int' => 'Description.',
'publicPropertyWithDefaultValue: float = 123.0' => '_No description provided_.',
'publicPromotedPropertyWithoutDefaultValue: int' => 'Description.',
'publicPromotedPropertyWithDefaultValue: int = 321' => '_No description provided_.',
],
$command->getInstructionParameters(
PreprocessTest__ParameterizableInstruction::class,
),
);
}
}

// @phpcs:disable PSR1.Classes.ClassDeclaration.MultipleClasses
// @phpcs:disable Squiz.Classes.ValidClassName.NotCamelCaps

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*
* @implements ParameterizableInstruction<PreprocessTest__ParameterizableInstructionParameters>
*/
class PreprocessTest__ParameterizableInstruction implements ParameterizableInstruction {
#[Override]
public static function getName(): string {
return 'test:parameterizable-instruction';
}

#[Override]
public static function getDescription(): string {
return 'Description description description description description.';
}

#[Override]
public static function getTargetDescription(): ?string {
return 'Target target target target target.';
}

#[Override]
public static function getParameters(): string {
return PreprocessTest__ParameterizableInstructionParameters::class;
}

/**
* @inheritDoc
*/
#[Override]
public static function getParametersDescription(): array {
return [];
}

#[Override]
public function process(string $path, string $target, Serializable $parameters): string {
return $path;
}
}

/**
* @internal
* @noinspection PhpMultipleClassesDeclarationsInOneFile
*/
class PreprocessTest__ParameterizableInstructionParameters implements Serializable {
public static bool $publicStaticProperty = true;

/**
* Description.
*/
public int $publicPropertyWithoutDefaultValue;
public float $publicPropertyWithDefaultValue = 123;

public function __construct(
/**
* Description.
*/
public int $publicPromotedPropertyWithoutDefaultValue,
public int $publicPromotedPropertyWithDefaultValue = 321,
protected bool $protectedProperty = true,
protected bool $privateProperty = true,
) {
$this->publicPropertyWithoutDefaultValue = 0;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ interface ParameterizableInstruction extends Instruction {
public static function getParameters(): string;

/**
* @return non-empty-array<string, string>
* @deprecated %{VERSION} Use docblock instead.
*
* @return array<string, string>
*/
public static function getParametersDescription(): array;

Expand Down
Loading

0 comments on commit 639792b

Please sign in to comment.