Skip to content

Commit

Permalink
Merge branch 'feature/cli' into develop
Browse files Browse the repository at this point in the history
  • Loading branch information
cerbero90 committed Jan 12, 2025
2 parents 0dc31a9 + 60b21fd commit 23430fa
Show file tree
Hide file tree
Showing 67 changed files with 2,762 additions and 158 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
matrix:
php: [8.1, 8.2, 8.3, 8.4]
dependency-version: [prefer-lowest, prefer-stable]
os: [ubuntu-latest, windows-latest]
os: [ubuntu-latest, windows-latest, macos-latest]

name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }} - ${{ matrix.os }}

Expand Down
243 changes: 191 additions & 52 deletions README.md

Large diffs are not rendered by default.

27 changes: 27 additions & 0 deletions bin/enum
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env php

<?php

use function Cerbero\Enum\fail;
use function Cerbero\Enum\path;
use function Cerbero\Enum\setPathsByOptions;
use function Cerbero\Enum\splitArgv;

is_file($autoload = dirname(__DIR__, 1) . '/vendor/autoload.php') && require $autoload;
is_file($autoload = dirname(__DIR__, 4) . '/vendor/autoload.php') && require $autoload;
is_file($autoload = dirname(__DIR__, 4) . '/enums.php') && require $autoload;

if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))) {
try {
[$arguments, $options] = splitArgv($argv);
setPathsByOptions($options);

$outcome = require $command;
} catch (Throwable $e) {
$outcome = fail($e->getMessage());
}

exit($outcome ? 0 : 1);
}

require path(__DIR__ . '/../cli/help');
25 changes: 25 additions & 0 deletions cli/annotate.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Cerbero\Enum\Enums;
use Cerbero\Enum\Services\Annotator;

use function Cerbero\Enum\enumOutcome;
use function Cerbero\Enum\normalizeEnums;
use function Cerbero\Enum\succeed;

$enums = array_intersect(['--all', '-a'], $options) ? [...Enums::namespaces()] : normalizeEnums($arguments);

if (empty($enums)) {
return succeed('No enums to annotate.');
}

$succeeded = true;
$force = !! array_intersect(['--force', '-f'], $options);

foreach ($enums as $enum) {
$succeeded = enumOutcome($enum, fn() => (new Annotator($enum))->annotate($force)) && $succeeded;
}

return $succeeded;
57 changes: 57 additions & 0 deletions cli/help
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
Annotate enums to ease IDE autocompletion.

Usage: enum annotate enum1 [enum2 ...]

Available options:

-a, --all Whether all enums should be annotated
-f, --force Whether existing annotations should be overwritten

Examples:
enum annotate App/Enums/MyEnum
enum annotate "App\Enums\MyEnum"
enum annotate App/Enums/MyEnum1 App/Enums/MyEnum2
enum annotate App/Enums/MyEnum --force
enum annotate --all
enum annotate --all --force

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Create a new enum.

Usage: enum make enum case1 case2

Available options:

--backed=VALUE How cases should be backed. VALUE is either:
snake|camel|kebab|upper|lower|int0|int1|bitwise
-f, --force Whether the existing enum should be overwritten
-t, --typescript Whether the enum should be synced in TypeScript

Examples:
enum make App/Enums/MyEnum Case1 Case2
enum make "App\Enums\MyEnum" Case1 Case2
enum make App/Enums/MyEnum Case1=value1 Case2=value2
enum make App/Enums/MyEnum Case1 Case2 --backed=int1
enum make App/Enums/MyEnum Case1 Case2 --force
enum make App/Enums/MyEnum Case1 Case2 --backed=bitwise --force
enum make App/Enums/MyEnum Case1 Case2 --typescript

――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――

Synchronize enums in TypeScript.

Usage: enum ts enum1 [enum2 ...]

Available options:

-a, --all Whether all enums should be synchronized
-f, --force Whether existing enums should be overwritten

Examples:
enum ts App/Enums/MyEnum
enum ts "App\Enums\MyEnum"
enum ts App/Enums/MyEnum1 App/Enums/MyEnum2
enum ts App/Enums/MyEnum --force
enum ts --all
enum ts --all --force
41 changes: 41 additions & 0 deletions cli/make.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

use Cerbero\Enum\Enums\Backed;
use Cerbero\Enum\Services\Generator;

use function Cerbero\Enum\enumOutcome;
use function Cerbero\Enum\fail;
use function Cerbero\Enum\option;
use function Cerbero\Enum\runAnnotate;
use function Cerbero\Enum\runTs;
use function Cerbero\Enum\succeed;

if (! $enum = strtr($arguments[0] ?? '', '/', '\\')) {
return fail('The name of the enum is missing.');
}

$force = !! array_intersect(['--force', '-f'], $options);

if (enum_exists($enum) && ! $force) {
return succeed("The enum {$enum} already exists.");
}

if (! $cases = array_slice($arguments, 1)) {
return fail('The cases of the enum are missing.');
}

try {
$generator = new Generator($enum, $cases, option('backed', $options));
} catch (ValueError) {
return fail('The option --backed supports only ' . implode(', ', Backed::names()));
}

$typeScript = !! array_intersect(['--typescript', '-t'], $options);

return enumOutcome($enum, function () use ($generator, $enum, $force, $typeScript) {
return $generator->generate($force)
&& runAnnotate($enum, $force)
&& ($typeScript ? runTs($enum, $force) : true);
});
25 changes: 25 additions & 0 deletions cli/ts.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

use Cerbero\Enum\Enums;
use Cerbero\Enum\Services\TypeScript;

use function Cerbero\Enum\enumOutcome;
use function Cerbero\Enum\normalizeEnums;
use function Cerbero\Enum\succeed;

$enums = array_intersect(['--all', '-a'], $options) ? [...Enums::namespaces()] : normalizeEnums($arguments);

if (empty($enums)) {
return succeed('No enums to synchronize.');
}

$succeeded = true;
$force = !! array_intersect(['--force', '-f'], $options);

foreach ($enums as $enum) {
$succeeded = enumOutcome($enum, fn() => (new TypeScript($enum))->sync($force)) && $succeeded;
}

return $succeeded;
11 changes: 9 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,20 @@
"autoload": {
"psr-4": {
"Cerbero\\Enum\\": "src"
}
},
"files": [
"helpers/core.php",
"helpers/cli.php"
]
},
"autoload-dev": {
"psr-4": {
"Cerbero\\Enum\\": "tests"
"Cerbero\\Enum\\": "tests",
"App\\": "tests/Skeleton/app",
"Domain\\": "tests/Skeleton/domain"
}
},
"bin": ["bin/enum"],
"scripts": {
"fix": "duster fix -u tlint,phpcodesniffer,pint",
"lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan",
Expand Down
178 changes: 178 additions & 0 deletions helpers/cli.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
<?php

declare(strict_types=1);

namespace Cerbero\Enum;

use Closure;
use Throwable;

/**
* Print out the given success message.
*/
function succeed(string $message): bool
{
fwrite(STDOUT, "\e[38;2;38;220;38m{$message}\e[0m" . PHP_EOL);

return true;
}

/**
* Print out the given error message.
*/
function fail(string $message): bool
{
fwrite(STDERR, "\e[38;2;220;38;38m{$message}\e[0m" . PHP_EOL);

return false;
}

/**
* Split the given argv into arguments and options.
*
* @param string[] $argv
* @return list<string[]>
*/
function splitArgv(array $argv): array
{
$arguments = $options = [];

foreach (array_slice($argv, 2) as $item) {
if (str_starts_with($item, '-')) {
$options[] = $item;
} else {
$arguments[] = $item;
}
}

return [$arguments, $options];
}

/**
* Set enum paths from the given options.
*
* @param string[] $options
*/
function setPathsByOptions(array $options): void
{
if ($basePath = option('base-path', $options)) {
Enums::setBasePath($basePath);
}

if ($paths = option('paths', $options)) {
Enums::setPaths(...explode(',', $paths));
}
}

/**
* Retrieve the value of the given option.
*
* @param string[] $options
*/
function option(string $name, array $options): ?string
{
$prefix = "--{$name}=";

foreach ($options as $option) {
if (str_starts_with($option, $prefix)) {
$segments = explode('=', $option, limit: 2);

return $segments[1] === '' ? null : $segments[1];
}
}

return null;
}

/**
* Retrieve the normalized namespaces of the given enums.
*
* @param list<string> $enums
* @return list<class-string<\UnitEnum>>
*/
function normalizeEnums(array $enums): array
{
$namespaces = array_map(fn(string $enum) => strtr($enum, '/', '\\'), $enums);

return array_unique(array_filter($namespaces, 'enum_exists'));
}

/**
* Print out the outcome of the given enum operation.
*
* @param class-string<\UnitEnum> $namespace
* @param Closure(): bool $callback
*/
function enumOutcome(string $enum, Closure $callback): bool
{
$error = null;

try {
$succeeded = $callback();
} catch (Throwable $e) {
$succeeded = false;
$error = "\e[38;2;220;38;38m{$e?->getMessage()}\e[0m";
}

if ($succeeded) {
fwrite(STDOUT, "\e[48;2;163;230;53m\e[38;2;63;98;18m\e[1m DONE \e[0m {$enum}" . PHP_EOL . PHP_EOL);
} else {
fwrite(STDERR, "\e[48;2;248;113;113m\e[38;2;153;27;27m\e[1m FAIL \e[0m {$enum} {$error}" . PHP_EOL . PHP_EOL);
}

return $succeeded;
}

/**
* Annotate the given enum within a new process.
*
* @param class-string<\UnitEnum> $enum
*/
function runAnnotate(string $enum, bool $force = false): bool
{
// Once an enum is loaded, PHP accesses it from the memory and not from the disk.
// Since we are writing on the disk, the enum in memory might get out of sync.
// To ensure that the annotations reflect the current content of such enum,
// we spin a new process to load in memory the latest state of the enum.
ob_start();

$succeeded = cli("annotate \"{$enum}\"" . ($force ? ' --force' : ''));

ob_end_clean();

return $succeeded;
}

/**
* Run the enum CLI in a new process.
*/
function cli(string $command, ?int &$status = null): bool
{
$cmd = vsprintf('"%s" "%s" %s 2>&1', [
PHP_BINARY,
path(__DIR__ . '/../bin/enum'),
$command,
]);

return passthru($cmd, $status) === null;
}

/**
* Synchronize the given enum in TypeScript within a new process.
*
* @param class-string<\UnitEnum> $enum
*/
function runTs(string $enum, bool $force = false): bool
{
// Once an enum is loaded, PHP accesses it from the memory and not from the disk.
// Since we are writing on the disk, the enum in memory might get out of sync.
// To make sure that we are synchronizing the current content of such enum,
// we spin a new process to load in memory the latest state of the enum.
ob_start();

$succeeded = cli("ts \"{$enum}\"" . ($force ? ' --force' : ''));

ob_end_clean();

return $succeeded;
}
Loading

0 comments on commit 23430fa

Please sign in to comment.