Skip to content

Methods for individual enums #24

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 30, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 43 additions & 10 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,32 +11,64 @@ jobs:
strategy:
fail-fast: true
matrix:
php: [ 8.1, 8.2, 8.3 ]
laravel: [ 10.*, 11.* ]
php: [ 8.1, 8.2, 8.3, 8.4 ]
laravel: [ 10.*, 11.*, 12.* ]
include:
- php: 8.1
laravel: 10.*
pest: 2.*
testbench: 8.*
larastan: 2.*
- php: 8.2
laravel: 10.*
pest: 2.*
testbench: 8.*
larastan: 2.*
- php: 8.3
laravel: 10.*
pest: 2.*
testbench: 8.*
larastan: 2.*
- php: 8.4
laravel: 10.*
pest: 2.*
testbench: 8.*
larastan: 2.*
- php: 8.2
laravel: 11.*
pest: 3.*
testbench: 9.*
larastan: 2.*
- php: 8.3
laravel: 11.*
pest: 3.*
testbench: 9.*
larastan: 2.*
- php: 8.4
laravel: 11.*
pest: 3.*
testbench: 9.*
larastan: 2.*
- php: 8.2
laravel: 12.*
pest: 3.*
testbench: 10.*
larastan: 3.*
- php: 8.3
laravel: 12.*
pest: 3.*
testbench: 10.*
larastan: 3.*
- php: 8.4
laravel: 12.*
pest: 3.*
testbench: 10.*
larastan: 3.*
exclude:
- php: 8.1
laravel: 11.*
- php: 8.1
laravel: 12.*

name: Paragon Tests - PHP ${{ matrix.php }} - Laravel ${{ matrix.laravel }}

Expand All @@ -45,7 +77,7 @@ jobs:
uses: actions/checkout@v4

- name: Cache dependencies
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: dependencies-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }}
Expand All @@ -61,9 +93,10 @@ jobs:
- name: Install dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update
composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update --dev
composer require "pestphp/pest:${{ matrix.pest }}" "pestphp/pest-plugin-laravel:${{ matrix.pest }}" "pestphp/pest-plugin-type-coverage:${{ matrix.pest }}" --no-interaction --no-update --dev
composer update --prefer-dist --no-interaction --no-suggest --dev
composer require "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update
composer require "pestphp/pest:${{ matrix.pest }}" "pestphp/pest-plugin-laravel:${{ matrix.pest }}" "pestphp/pest-plugin-type-coverage:${{ matrix.pest }}" --no-interaction --no-update
composer require "larastan/larastan:${{ matrix.larastan }}" --no-interaction --no-update
composer update --prefer-dist --no-interaction
composer dump

- name: Execute tests
Expand All @@ -79,7 +112,7 @@ jobs:
uses: actions/checkout@v4

- name: Cache dependencies
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: dependencies-composer-${{ hashFiles('composer.json') }}
Expand All @@ -92,7 +125,7 @@ jobs:

- name: Install dependencies
run: |
composer install --no-interaction --no-suggest --dev
composer install --no-interaction
composer dump

- name: Execute Pint
Expand All @@ -108,7 +141,7 @@ jobs:
uses: actions/checkout@v4

- name: Cache dependencies
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ~/.composer/cache/files
key: dependencies-composer-${{ hashFiles('composer.json') }}
Expand All @@ -121,7 +154,7 @@ jobs:

- name: Install dependencies
run: |
composer install --no-interaction --no-suggest --dev
composer install --no-interaction
composer dump

- name: Execute Larastan
Expand Down
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,15 @@ While this package ignores static methods on the PHP Enums, we allow you to crea
php artisan paragon:enum:add-method
```

This will create a new file at `resources/js/vendors/paragon/enums` containing a method. You are free to do whatever you need inside this file. You have direct access to `this.items` which allows you to interact with the enum cases in whatever way you need. Just keep in mind that because the items are "frozen", you can't mutate them directly. An example would be to have a method that automatically generates a select list from your Enum.
You will be prompted to search for the enum this method should belong to. It will be placed inside a directory that matches the namespace of the enum. For example, an enum at `App\Enums\Status` would place an enum method file at `resources/js/vendors/paragon/enums/App/Enums/Status`. You are free to do whatever you need inside this file. You have direct access to `this.items` which allows you to interact with the enum cases in whatever way you need. Just keep in mind that because the items are "frozen", you can't mutate them directly. An example would be to have a method that automatically generates a select list from your Enum.

If you need to create a method that is available to every enum, just create an enum method with the `--global` or `-g` flag.

```bash
php artisan paragon:enum-method --global
```

This will create a new file at `resources/js/vendors/paragon/enums` containing a method.

### Ignoring Enums Or Public Methods

Expand Down
13 changes: 11 additions & 2 deletions src/Commands/GenerateEnumsCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,26 @@ public function handle(): int
{
$builder = $this->builder();

$this->generateEnums($builder);
$this->generateAbstractEnum($builder);

return self::SUCCESS;
}

protected function generateEnums(EnumBuilder $builder): void
{
$generatedEnums = $this->enums()
->map(fn (ReflectionEnum $enum) => app(EnumGenerator::class, ['enum' => $enum, 'builder' => $builder])())
->filter();

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

protected function generateAbstractEnum(EnumBuilder $builder): void
{
app(AbstractEnumGenerator::class, ['builder' => $builder])();

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

return self::SUCCESS;
}

/**
Expand Down
125 changes: 118 additions & 7 deletions src/Commands/MakeEnumMethodCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
use Exception;
use Illuminate\Console\GeneratorCommand;
use Illuminate\Contracts\Filesystem\FileNotFoundException;
use Illuminate\Support\Str;
use InvalidArgumentException;
use Kirschbaum\Paragon\Concerns\Builders\EnumBuilder;
use Kirschbaum\Paragon\Concerns\Builders\EnumJsBuilder;
use Kirschbaum\Paragon\Concerns\Builders\EnumTsBuilder;
use Kirschbaum\Paragon\Concerns\DiscoverEnums;
use Kirschbaum\Paragon\Generators\AbstractEnumGenerator;
use Kirschbaum\Paragon\Generators\EnumGenerator;
use ReflectionEnum;
use ReflectionException;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Throwable;
use UnitEnum;

use function Laravel\Prompts\search;
use function Laravel\Prompts\text;

#[AsCommand(name: 'paragon:enum:add-method', description: 'Create a new global typescript method to be applied to every generated enum')]
Expand All @@ -21,16 +32,16 @@ class MakeEnumMethodCommand extends GeneratorCommand
/**
* Execute the console command.
*
* @throws Exception
* @throws FileNotFoundException
* @throws Throwable
*/
public function handle(): ?bool
{
parent::handle();

app(AbstractEnumGenerator::class, ['builder' => $this->builder()])();
$this->runGenerator();

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

return true;
}
Expand Down Expand Up @@ -70,6 +81,38 @@ protected function promptForMissingArgumentsUsing(): array
];
}

/**
* Interact further with the user if they were prompted for missing arguments.
*
* @phpcsSuppress SlevomatCodingStandard.Functions.UnusedParameter
*/
protected function afterPromptingForMissingArguments(InputInterface $input, OutputInterface $output): void
{
if (
is_string($this->option('enum'))
|| $this->option('global')
) {
return;
}

/**
* @var string $enumPath
*/
$enumPath = config('paragon.enums.paths.php');

$enums = DiscoverEnums::within(app_path($enumPath))
->mapWithKeys(fn (ReflectionEnum $reflector) => [$reflector->getName() => $reflector->getName()]);

$enum = search(
label: 'Which enum should this method be created for?',
options: fn (string $value) => strlen($value) > 0
? $enums->filter(fn (string $enum): bool => Str::contains($enum, $value, ignoreCase: true))->all()
: []
);

$input->setOption('enum', $enum);
}

/**
* Build the file with the given name.
*
Expand All @@ -88,15 +131,30 @@ protected function buildClass($name): string
/**
* Get the destination class path.
*
* @throws Exception
* @throws Throwable
*/
protected function getPath($name): string
{
/** @var string */
$methods = config('paragon.enums.paths.methods');
/**
* @var string $path
*/
$path = config('paragon.enums.paths.methods');
$extension = $this->option('javascript') ? 'js' : 'ts';

return resource_path($methods) . "/{$this->name()}.{$extension}";
if (! $this->option('global')) {
$enum = str($this->enumOption())->replace('/', '\\');

throw_unless(
enum_exists($enum->toString()),
ReflectionException::class,
"Class \"{$enum}\" does not exist"
);

$path = $enum->replace('\\', '/')
->prepend(Str::finish($path, '/'));
}

return resource_path($path) . "/{$this->name()}.{$extension}";
}

/**
Expand All @@ -120,7 +178,48 @@ protected function builder(): EnumBuilder
return $this->option('javascript')
? app(EnumJsBuilder::class)
: app(EnumTsBuilder::class);
}

/**
* @throws ReflectionException
* @throws Throwable
*/
protected function runGenerator(): void
{
$this->option('global')
? app(AbstractEnumGenerator::class, ['builder' => $this->builder()])()
: app(EnumGenerator::class, [
'enum' => new ReflectionEnum($this->enumOption()),
'builder' => $this->builder(),
'forceRegenerate' => true,
])();
}

protected function writeInfo(): void
{
$name = $this->option('global')
? 'Abstract'
: Str::afterLast($this->enumOption(), '\\');

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

/**
* @return class-string<UnitEnum>
*
* @throws Throwable
*/
protected function enumOption(): string
{
$enum = $this->option('enum');

throw_unless(
is_string($enum) && is_a($enum, UnitEnum::class, true),
InvalidArgumentException::class,
'The enum option must be a valid class-string of a UnitEnum'
);

return $enum;
}

/**
Expand All @@ -131,6 +230,18 @@ protected function builder(): EnumBuilder
protected function getOptions(): array
{
return [
new InputOption(
name: 'enum',
shortcut: 'e',
mode: InputOption::VALUE_REQUIRED,
description: 'Fully qualified namespace of enum to use',
),
new InputOption(
name: 'global',
shortcut: 'g',
mode: InputOption::VALUE_NONE,
description: 'Create global enum method',
),
new InputOption(
name: 'javascript',
shortcut: 'j',
Expand Down
2 changes: 1 addition & 1 deletion src/Concerns/DiscoverEnums.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
use Illuminate\Support\Collection;
use ReflectionEnum;
use ReflectionException;
use SplFileInfo;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Finder\SplFileInfo;
use UnitEnum;

class DiscoverEnums
Expand Down
Loading