Skip to content

Commit

Permalink
Merge pull request #3 from suleymanozev/main
Browse files Browse the repository at this point in the history
[FEATURE] Added enum:annotate command
  • Loading branch information
trippo authored Mar 10, 2023
2 parents 8f41035 + 6edc5a4 commit 9f00582
Show file tree
Hide file tree
Showing 9 changed files with 397 additions and 3 deletions.
3 changes: 1 addition & 2 deletions .php-cs-fixer.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,7 @@
'no_spaces_after_function_name' => true,
'no_spaces_around_offset' => true,
'no_spaces_inside_parenthesis' => true,
'no_trailing_comma_in_list_call' => true,
'no_trailing_comma_in_singleline_array' => true,
'no_trailing_comma_in_singleline' => true,
'no_trailing_whitespace' => true,
'no_trailing_whitespace_in_comment' => true,
'no_unneeded_control_parentheses' => true,
Expand Down
12 changes: 11 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,12 @@
},
"require": {
"php": "^8.1",
"composer/class-map-generator": "^1.0",
"datomatic/enum-helper": "^1.0",
"illuminate/translation": "^8.0|^9.0|^10.0",
"illuminate/support": "^8.0|^9.0|^10.0"
"illuminate/support": "^8.0|^9.0|^10.0",
"laminas/laminas-code": "^4.0",
"jawira/case-converter": "^3.5"
},
"require-dev": {
"pestphp/pest": "^1.21",
Expand All @@ -50,5 +53,12 @@
"@test:unit"
],
"coverage": "@test:unit --coverage"
},
"extra": {
"laravel": {
"providers": [
"Datomatic\\LaravelEnumHelper\\LaravelEnumHelperServiceProvider"
]
}
}
}
217 changes: 217 additions & 0 deletions src/Commands/EnumAnnotateCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php

declare(strict_types=1);

namespace Datomatic\LaravelEnumHelper\Commands;

use Composer\ClassMapGenerator\ClassMapGenerator;
use Illuminate\Console\Command;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Filesystem\Filesystem;
use Jawira\CaseConverter\Convert;
use Laminas\Code\Generator\DocBlock\Tag\MethodTag;
use Laminas\Code\Generator\DocBlock\Tag\TagInterface;
use Laminas\Code\Generator\DocBlockGenerator;
use Laminas\Code\Reflection\DocBlockReflection;
use ReflectionClass;
use ReflectionException;
use UnitEnum;

class EnumAnnotateCommand extends Command
{
protected $signature = 'enum:annotate
{class?}
{--folder=}';

protected $description = 'Generate DocBlock annotations for enum classes';

protected Filesystem $filesystem;

/**
* @throws ReflectionException|FileNotFoundException
*/
public function handle(Filesystem $filesystem): int
{
$this->filesystem = $filesystem;

$class = $this->argument('class');
if (is_string($class)) {
return$this->annotateClass($class);
}

return $this->annotateFolder();
}

/**
* @throws ReflectionException|FileNotFoundException
*/
protected function annotateFolder(): int
{
$searchDirectory = $this->searchDirectory();
$searchDirectoryMap = ClassMapGenerator::createMap($searchDirectory);

if (count($searchDirectoryMap) > 0) {
foreach ($searchDirectoryMap as $class => $_) {
$reflection = new ReflectionClass($class);

if ($reflection->isSubclassOf(UnitEnum::class)) {
$this->annotate($reflection);
}
}

return self::SUCCESS;
}

$this->warn("Please create enum within '{$searchDirectory}'");

return self::FAILURE;
}

/**
* @throws ReflectionException|FileNotFoundException
*/
protected function annotateClass(string $className): int
{
if (! is_subclass_of($className, UnitEnum::class)) {
$parentClass = UnitEnum::class;
$this->error("The given class {$className} must be an instance of {$parentClass}.");

return self::FAILURE;
}

$reflection = new ReflectionClass($className);
$this->annotate($reflection);

return self::SUCCESS;
}

/**
* @param ReflectionClass<UnitEnum> $reflectionClass
* @throws FileNotFoundException
*/
protected function annotate(ReflectionClass $reflectionClass): void
{
$docBlock = new DocBlockGenerator;

if ($reflectionClass->getDocComment()) {
$docBlock->setShortDescription(
DocBlockGenerator::fromReflection(new DocBlockReflection($reflectionClass))
->getShortDescription()
);
}

$this->updateClassDocblock($reflectionClass, $this->getDocBlock($reflectionClass));
}

/**
* @throws FileNotFoundException
*/
protected function updateClassDocblock(ReflectionClass $reflectionClass, DocBlockGenerator $docBlock): void
{
$shortName = $reflectionClass->getShortName();
$fileName = (string) $reflectionClass->getFileName();
$contents = $this->filesystem->get($fileName);

$enumDeclaration = "enum {$shortName}";

// Remove existing docblock
$quotedClassDeclaration = preg_quote($enumDeclaration);
$contents = preg_replace(
"#\\r?\\n?/\*[\s\S]*?\*/(\\r?\\n)?{$quotedClassDeclaration}#ms",
"\$1{$enumDeclaration}",
$contents
);
if ($contents) {
$enumDeclarationPos = strpos($contents, $enumDeclaration);
if (! is_bool($enumDeclarationPos)) {
// Make sure we don't replace too much
$contents = substr_replace(
$contents,
"{$docBlock->generate()}{$enumDeclaration}",
$enumDeclarationPos,
strlen($enumDeclaration)
);
}

$this->filesystem->put($fileName, $contents);
$this->info("Wrote new phpDocBlock to {$fileName}.");
}
}

protected function getDocBlock(ReflectionClass $reflectionClass): DocBlockGenerator
{
$docBlock = (new DocBlockGenerator)
->setWordWrap(false);

$originalDocBlock = null;

if ($reflectionClass->getDocComment()) {
$originalDocBlock = DocBlockGenerator::fromReflection(
new DocBlockReflection(ltrim($reflectionClass->getDocComment()))
);

if ($originalDocBlock->getShortDescription()) {
$docBlock->setShortDescription($originalDocBlock->getShortDescription());
}

if ($originalDocBlock->getLongDescription()) {
$docBlock->setLongDescription($originalDocBlock->getLongDescription());
}
}

$docBlock->setTags($this->getDocblockTags(
$originalDocBlock,
$reflectionClass
));

return $docBlock;
}

/**
* @return array<TagInterface>
*/
protected function getDocblockTags(
DocBlockGenerator|null $originalDocblock,
ReflectionClass $reflectionClass
): array {
$constants = $reflectionClass->getConstants();
$constantKeys = array_keys($constants);

$tags = array_map(
static fn (mixed $value, string $constantName): MethodTag => new MethodTag(
(new Convert($constantName))->toCamel(),
['string'],
null,
true
),
$constants,
$constantKeys,
);

if ($originalDocblock) {
$tags = array_merge(
$tags,
array_filter($originalDocblock->getTags(), function (TagInterface $tag) use ($constantKeys): bool {
return ! $tag instanceof MethodTag
|| ! in_array(
(new Convert((string) $tag->getMethodName()))->toCamel(),
array_map(fn ($constantName) => (new Convert($constantName))->toCamel(), $constantKeys),
true
);
})
);
}

return $tags;
}

protected function searchDirectory(): string
{
$folder = $this->option('folder');
if (is_string($folder)) {
return $folder;
}

return app_path('Enums');
}
}
25 changes: 25 additions & 0 deletions src/LaravelEnumHelperServiceProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Datomatic\LaravelEnumHelper;

use Datomatic\LaravelEnumHelper\Commands\EnumAnnotateCommand;
use Illuminate\Support\ServiceProvider;

class LaravelEnumHelperServiceProvider extends ServiceProvider
{
public function boot(): void
{
$this->bootCommands();
}

protected function bootCommands(): void
{
if ($this->app->runningInConsole()) {
$this->commands([
EnumAnnotateCommand::class,
]);
}
}
}
82 changes: 82 additions & 0 deletions tests/Feature/EnumAnnotateCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

beforeEach(function () {
if (! file_exists(app_path('Enums'))) {
mkdir(app_path('Enums'), 0755, true);
}
if (! file_exists($this->withoutDocBlockEnumsFolder)) {
mkdir($this->withoutDocBlockEnumsFolder, 0755, true);
}
copy(
__DIR__ . '/../stubs/StatusWithoutDocBlock.stub',
$this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php'
);
});

afterEach(function () {
if (file_exists($this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php')) {
unlink($this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php');
}
rmdir($this->withoutDocBlockEnumsFolder);
rmdir(app_path('Enums'));
});

it('can be success single file', function () {
$this->artisan("enum:annotate --folder={$this->withoutDocBlockEnumsFolder} Datomatic\\\\LaravelEnumHelper\\\\Tests\\\\Support\\\\WithoutDocBlockEnums\\\\StatusWithoutDocBlock")
->assertSuccessful();
$contents = file_get_contents($this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php');
$this->assertEquals(1, substr_count($contents, '@method static string pending()'));
$this->assertEquals(1, substr_count($contents, '@method static string accepted()'));
$this->assertEquals(1, substr_count($contents, '@method static string discarded()'));
$this->assertEquals(1, substr_count($contents, '@method static string noResponse()'));
});

it('can be success single file with exists doc block', function () {
$this->artisan("enum:annotate --folder={$this->enumsFolder} Datomatic\\\\LaravelEnumHelper\\\\Tests\\\\Support\\\\Enums\\\\Status")
->assertSuccessful();
$contents = file_get_contents($this->enumsFolder . '/Status.php');
$this->assertEquals(1, substr_count($contents, '@method static string pending()'));
$this->assertEquals(1, substr_count($contents, '@method static string accepted()'));
$this->assertEquals(1, substr_count($contents, '@method static string discarded()'));
$this->assertEquals(1, substr_count($contents, '@method static string noResponse()'));
});

it('can be success single file with exists doc block without method tags', function () {
copy(__DIR__ . '/../stubs/StatusWithoutMethodTagDocBlock.stub', $this->withoutDocBlockEnumsFolder . '/StatusWithoutMethodTagDocBlock.php');
$this->artisan("enum:annotate --folder={$this->withoutDocBlockEnumsFolder} Datomatic\\\\LaravelEnumHelper\\\\Tests\\\\Support\\\\WithoutDocBlockEnums\\\\StatusWithoutMethodTagDocBlock")
->assertSuccessful();
$contents = file_get_contents($this->withoutDocBlockEnumsFolder . '/StatusWithoutMethodTagDocBlock.php');
$this->assertEquals(1, substr_count($contents, '@method static string pending()'));
$this->assertEquals(1, substr_count($contents, '@method static string accepted()'));
$this->assertEquals(1, substr_count($contents, '@method static string discarded()'));
$this->assertEquals(1, substr_count($contents, '@method static string noResponse()'));
unlink($this->withoutDocBlockEnumsFolder . '/StatusWithoutMethodTagDocBlock.php');
});

it('can be success whole folder', function () {
$this->artisan("enum:annotate --folder={$this->withoutDocBlockEnumsFolder}")
->assertSuccessful();
$contents = file_get_contents($this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php');
$this->assertEquals(1, substr_count($contents, '@method static string pending()'));
$this->assertEquals(1, substr_count($contents, '@method static string accepted()'));
$this->assertEquals(1, substr_count($contents, '@method static string discarded()'));
$this->assertEquals(1, substr_count($contents, '@method static string noResponse()'));
});

it('can be failed with class', function () {
$this->artisan('enum:annotate Datomatic\\\\LaravelEnumHelper\\\\Tests\\\\Support\\\\NotEnums\\\\TestClass')
->assertFailed();
});

it('can be failed with without any argument or option with empty app enums folder', function () {
$this->artisan('enum:annotate')
->assertFailed();
});

it('can be failed with empty folder', function () {
unlink($this->withoutDocBlockEnumsFolder . '/StatusWithoutDocBlock.php');
$this->artisan("enum:annotate --folder={$this->withoutDocBlockEnumsFolder}")
->assertFailed();
});
9 changes: 9 additions & 0 deletions tests/Support/NotEnums/TestClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?php

declare(strict_types=1);

namespace Datomatic\LaravelEnumHelper\Tests\Support\NotEnums;

class TestClass
{
}
11 changes: 11 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,23 @@

namespace Datomatic\LaravelEnumHelper\Tests;

use Datomatic\LaravelEnumHelper\LaravelEnumHelperServiceProvider;
use Orchestra\Testbench\TestCase as TestbenchTestCase;

class TestCase extends TestbenchTestCase
{
protected string $withoutDocBlockEnumsFolder = __DIR__ . '/Support/WithoutDocBlockEnums';
protected string $enumsFolder = __DIR__ . '/Support/Enums';

protected function getEnvironmentSetUp($app)
{
$app['path.lang'] = __DIR__ . '/lang';
}

protected function getPackageProviders($app): array
{
return [
LaravelEnumHelperServiceProvider::class,
];
}
}
Loading

0 comments on commit 9f00582

Please sign in to comment.