Skip to content

Commit

Permalink
Paragon Enums
Browse files Browse the repository at this point in the history
  • Loading branch information
brandonferens committed Sep 23, 2024
1 parent be40536 commit 6f3d474
Show file tree
Hide file tree
Showing 14 changed files with 741 additions and 1 deletion.
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/.idea
/vendor
composer.phar
composer.lock
phpunit.xml
.phpunit.result.cache
.DS_Store
Thumbs.db
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1 @@
# renum
# Paragon
38 changes: 38 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "kirschbaum-development/paragon",
"description": "A Laravel package for generating enum-like objects in typescript based on PHP enum classes.",
"keywords": [
"laravel",
"actions",
"events"
],
"homepage": "https://github.com/kirschbaum-development/paragon",
"license": "MIT",
"authors": [
{
"name": "Brandon Ferens",
"email": "brandon@kirschbaumdevelopment.com",
"role": "Developer"
}
],
"require": {
"php": "^8.0"
},
"autoload": {
"psr-4": {
"Kirschbaum\\Paragon\\": "src/"
}
},
"config": {
"sort-packages": true
},
"minimum-stability": "dev",
"prefer-stable": true,
"extra": {
"laravel": {
"providers": [
"Kirschbaum\\Paragon\\ParagonServiceProvider"
]
}
}
}
13 changes: 13 additions & 0 deletions config/paragon.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

return [
'enums' => [
'abstract-class' => 'Enum',

'paths' => [
'php' => app_path(),
'generated' => 'js/enums',
'methods' => 'js/vendors/paragon/enums',
],
],
];
43 changes: 43 additions & 0 deletions src/Commands/GenerateEnumsCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Kirschbaum\Paragon\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Collection;
use Kirschbaum\Paragon\Concerns\DiscoverEnums;
use Kirschbaum\Paragon\Generators\AbstractEnumGenerator;
use Kirschbaum\Paragon\Generators\EnumGenerator;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(name: 'paragon:generate-enums', description: 'Generate Typescript versions of existing PHP enums')]
class GenerateEnumsCommand extends Command
{
/**
* Execute the console command.
*/
public function handle(): int
{
$generatedEnums = $this->enums()
->map(function ($enum) {
return EnumGenerator::generate($enum);
})
->filter();

$this->components->info("{$generatedEnums->count()} enums have been (re)generated.");

AbstractEnumGenerator::generate();

$this->components->info('Abstract enum class has been (re)generated.');

return self::SUCCESS;
}

/**
* Gather all enum namespaces for searching.
*/
protected function enums(): Collection
{
return DiscoverEnums::within(config('paragon.enums.paths.php'))
->values();
}
}
84 changes: 84 additions & 0 deletions src/Commands/MakeEnumMethodCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

namespace Kirschbaum\Paragon\Commands;

use Illuminate\Console\Command;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Kirschbaum\Paragon\Generators\AbstractEnumGenerator;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;

use function Laravel\Prompts\text;

#[AsCommand(name: 'paragon:enum-method', description: 'Create a new typescript enum method')]
class MakeEnumMethodCommand extends GeneratorCommand
{
/**
* Get the stub file for the generator.
*/
protected function getStub(): string
{
return __DIR__ . '/../../stubs/method.stub';
}

/**
* Execute the console command.
*
* @throws FileNotFoundException
*/
public function handle(): int
{
parent::handle();

AbstractEnumGenerator::generate();

$this->components
->info("Abstract enum class has been rebuilt to include new [{$this->argument('name')}] method.");

return self::SUCCESS;
}

/**
* Get the console command arguments.
*/
protected function getArguments(): array
{
return [
['name', InputArgument::REQUIRED, 'The name of the enum method'],
];
}

/**
* Interact further with the user if they were prompted for missing arguments.
*/
protected function promptForMissingArgumentsUsing(): array
{
return [
'name' => fn () => text(
label: 'What is the name of the new enum method?',
placeholder: 'e.g. asOptions',
),
];
}

/**
* Build the file with the given name.
*
* @throws FileNotFoundException
*/
protected function buildClass($name): string
{
$stub = $this->files->get($this->getStub());

return str_replace('{{ Method }}', $this->argument('name'), $stub);
}

/**
* Get the destination class path.
*/
protected function getPath($name): string
{
return resource_path(config('paragon.enums.paths.methods')) . DIRECTORY_SEPARATOR . $this->argument('name') . '.ts';
}
}
55 changes: 55 additions & 0 deletions src/Concerns/DiscoverEnums.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
<?php

namespace Kirschbaum\Paragon\Concerns;

use Illuminate\Support\Collection;
use Illuminate\Support\Str;
use ReflectionClass;
use ReflectionException;
use SplFileInfo;
use Symfony\Component\Finder\Finder;

class DiscoverEnums
{
/**
* Get all the enums by searching the given directory.
*/
public static function within(array|string $path): Collection
{
return static::getEnums(Finder::create()->files()->in($path));
}

/**
* Filter the files down to only enums.
*/
protected static function getEnums($files): Collection
{
return collect($files)
->mapWithKeys(function ($file) {
try {
$reflector = new ReflectionClass($enum = static::classFromFile($file));
} catch (ReflectionException) {
return [];
}

return $reflector->isEnum()
? [$enum => $enum]
: [];
})
->filter();
}

/**
* Extract the class name from the given file path.
*/
protected static function classFromFile(SplFileInfo $file): string
{
$class = trim(Str::replaceFirst(base_path(), '', $file->getRealPath()), DIRECTORY_SEPARATOR);

return str_replace(
[DIRECTORY_SEPARATOR, ucfirst(basename(app()->path())) . '\\'],
['\\', app()->getNamespace()],
ucfirst(Str::replaceLast('.php', '', $class))
);
}
}
8 changes: 8 additions & 0 deletions src/Concerns/IgnoreWhenGeneratingTypescript.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Kirschbaum\Paragon\Concerns;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class IgnoreWhenGeneratingTypescript {}
104 changes: 104 additions & 0 deletions src/Generators/AbstractEnumGenerator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
<?php

namespace Kirschbaum\Paragon\Generators;

use Illuminate\Contracts\Filesystem\Filesystem;
use Illuminate\Support\Collection;
use Illuminate\Support\Facades\Storage;
use Symfony\Component\Finder\Exception\DirectoryNotFoundException;
use Symfony\Component\Finder\Finder;

class AbstractEnumGenerator
{
protected Filesystem $files;

public function __construct()
{
$this->files = Storage::createLocalDriver([
'root' => resource_path(config('paragon.enums.paths.generated')),
]);
}

public function __invoke(): void
{
$this->files->put($this->path(), $this->contents());
}

public static function generate()
{
return (new self())();
}

/**
* Inject all prepared data into the stub.
*/
protected function contents(): string
{
$imports = $this->imports();
$suffix = $imports->count() ? PHP_EOL : '';

return str(file_get_contents($this->stubPath()))
->replace('{{ Abstract }}', config('paragon.enums.abstract-class'))
->replace('{{ Imports }}', "{$imports->join('')}{$suffix}")
->replace('{{ Methods }}', "{$this->methods($imports->keys())}{$suffix}");
}

/**
* Get the path to the stubs.
*/
public function stubPath(): string
{
return __DIR__ . '/../../stubs/abstract-enum.stub';
}

/**
* Build out the actual enum case object including the name, value if needed, and any public methods.
*/
protected function imports(): Collection
{
try {
$files = Finder::create()
->files()
->in(resource_path(config('paragon.enums.paths.methods')));
} catch (DirectoryNotFoundException) {
return collect();
}

return collect($files)
->mapWithKeys(function ($file) {
$abstractPath = config('paragon.enums.paths.generated');
$filePath = str($file->getPath())
->after(resource_path())
->ltrim('/')
->explode('/');

$relativePath = collect(explode('/', $abstractPath))
->diff($filePath)
->map(fn () => '..')
->merge($filePath->diff(explode('/', $abstractPath)))
->join('/');

$name = (string) str($file->getFileName())->before('.');

return [$name => "import {$name} from '{$relativePath}/{$file->getFilename()}';" . PHP_EOL];
})
->sort();
}

/**
* Build out the actual enum case object including the name, value if needed, and any public methods.
*/
protected function methods(Collection $methods): string
{
return $methods->map(fn ($method) => PHP_EOL . "Enum.{$method} = {$method};")
->join('');
}

/**
* Path where the enum will be saved.
*/
protected function path(): string
{
return config('paragon.enums.abstract-class') . '.ts';
}
}
Loading

0 comments on commit 6f3d474

Please sign in to comment.