-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #3 from suleymanozev/main
[FEATURE] Added enum:annotate command
- Loading branch information
Showing
9 changed files
with
397 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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'); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
]); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
{ | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.