Skip to content

Commit 23430fa

Browse files
committed
Merge branch 'feature/cli' into develop
2 parents 0dc31a9 + 60b21fd commit 23430fa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

67 files changed

+2762
-158
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ jobs:
1313
matrix:
1414
php: [8.1, 8.2, 8.3, 8.4]
1515
dependency-version: [prefer-lowest, prefer-stable]
16-
os: [ubuntu-latest, windows-latest]
16+
os: [ubuntu-latest, windows-latest, macos-latest]
1717

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

README.md

Lines changed: 191 additions & 52 deletions
Large diffs are not rendered by default.

bin/enum

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
#!/usr/bin/env php
2+
3+
<?php
4+
5+
use function Cerbero\Enum\fail;
6+
use function Cerbero\Enum\path;
7+
use function Cerbero\Enum\setPathsByOptions;
8+
use function Cerbero\Enum\splitArgv;
9+
10+
is_file($autoload = dirname(__DIR__, 1) . '/vendor/autoload.php') && require $autoload;
11+
is_file($autoload = dirname(__DIR__, 4) . '/vendor/autoload.php') && require $autoload;
12+
is_file($autoload = dirname(__DIR__, 4) . '/enums.php') && require $autoload;
13+
14+
if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))) {
15+
try {
16+
[$arguments, $options] = splitArgv($argv);
17+
setPathsByOptions($options);
18+
19+
$outcome = require $command;
20+
} catch (Throwable $e) {
21+
$outcome = fail($e->getMessage());
22+
}
23+
24+
exit($outcome ? 0 : 1);
25+
}
26+
27+
require path(__DIR__ . '/../cli/help');

cli/annotate.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Cerbero\Enum\Enums;
6+
use Cerbero\Enum\Services\Annotator;
7+
8+
use function Cerbero\Enum\enumOutcome;
9+
use function Cerbero\Enum\normalizeEnums;
10+
use function Cerbero\Enum\succeed;
11+
12+
$enums = array_intersect(['--all', '-a'], $options) ? [...Enums::namespaces()] : normalizeEnums($arguments);
13+
14+
if (empty($enums)) {
15+
return succeed('No enums to annotate.');
16+
}
17+
18+
$succeeded = true;
19+
$force = !! array_intersect(['--force', '-f'], $options);
20+
21+
foreach ($enums as $enum) {
22+
$succeeded = enumOutcome($enum, fn() => (new Annotator($enum))->annotate($force)) && $succeeded;
23+
}
24+
25+
return $succeeded;

cli/help

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
Annotate enums to ease IDE autocompletion.
2+
3+
Usage: enum annotate enum1 [enum2 ...]
4+
5+
Available options:
6+
7+
-a, --all Whether all enums should be annotated
8+
-f, --force Whether existing annotations should be overwritten
9+
10+
Examples:
11+
enum annotate App/Enums/MyEnum
12+
enum annotate "App\Enums\MyEnum"
13+
enum annotate App/Enums/MyEnum1 App/Enums/MyEnum2
14+
enum annotate App/Enums/MyEnum --force
15+
enum annotate --all
16+
enum annotate --all --force
17+
18+
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
19+
20+
Create a new enum.
21+
22+
Usage: enum make enum case1 case2
23+
24+
Available options:
25+
26+
--backed=VALUE How cases should be backed. VALUE is either:
27+
snake|camel|kebab|upper|lower|int0|int1|bitwise
28+
-f, --force Whether the existing enum should be overwritten
29+
-t, --typescript Whether the enum should be synced in TypeScript
30+
31+
Examples:
32+
enum make App/Enums/MyEnum Case1 Case2
33+
enum make "App\Enums\MyEnum" Case1 Case2
34+
enum make App/Enums/MyEnum Case1=value1 Case2=value2
35+
enum make App/Enums/MyEnum Case1 Case2 --backed=int1
36+
enum make App/Enums/MyEnum Case1 Case2 --force
37+
enum make App/Enums/MyEnum Case1 Case2 --backed=bitwise --force
38+
enum make App/Enums/MyEnum Case1 Case2 --typescript
39+
40+
――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――――
41+
42+
Synchronize enums in TypeScript.
43+
44+
Usage: enum ts enum1 [enum2 ...]
45+
46+
Available options:
47+
48+
-a, --all Whether all enums should be synchronized
49+
-f, --force Whether existing enums should be overwritten
50+
51+
Examples:
52+
enum ts App/Enums/MyEnum
53+
enum ts "App\Enums\MyEnum"
54+
enum ts App/Enums/MyEnum1 App/Enums/MyEnum2
55+
enum ts App/Enums/MyEnum --force
56+
enum ts --all
57+
enum ts --all --force

cli/make.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Cerbero\Enum\Enums\Backed;
6+
use Cerbero\Enum\Services\Generator;
7+
8+
use function Cerbero\Enum\enumOutcome;
9+
use function Cerbero\Enum\fail;
10+
use function Cerbero\Enum\option;
11+
use function Cerbero\Enum\runAnnotate;
12+
use function Cerbero\Enum\runTs;
13+
use function Cerbero\Enum\succeed;
14+
15+
if (! $enum = strtr($arguments[0] ?? '', '/', '\\')) {
16+
return fail('The name of the enum is missing.');
17+
}
18+
19+
$force = !! array_intersect(['--force', '-f'], $options);
20+
21+
if (enum_exists($enum) && ! $force) {
22+
return succeed("The enum {$enum} already exists.");
23+
}
24+
25+
if (! $cases = array_slice($arguments, 1)) {
26+
return fail('The cases of the enum are missing.');
27+
}
28+
29+
try {
30+
$generator = new Generator($enum, $cases, option('backed', $options));
31+
} catch (ValueError) {
32+
return fail('The option --backed supports only ' . implode(', ', Backed::names()));
33+
}
34+
35+
$typeScript = !! array_intersect(['--typescript', '-t'], $options);
36+
37+
return enumOutcome($enum, function () use ($generator, $enum, $force, $typeScript) {
38+
return $generator->generate($force)
39+
&& runAnnotate($enum, $force)
40+
&& ($typeScript ? runTs($enum, $force) : true);
41+
});

cli/ts.php

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Cerbero\Enum\Enums;
6+
use Cerbero\Enum\Services\TypeScript;
7+
8+
use function Cerbero\Enum\enumOutcome;
9+
use function Cerbero\Enum\normalizeEnums;
10+
use function Cerbero\Enum\succeed;
11+
12+
$enums = array_intersect(['--all', '-a'], $options) ? [...Enums::namespaces()] : normalizeEnums($arguments);
13+
14+
if (empty($enums)) {
15+
return succeed('No enums to synchronize.');
16+
}
17+
18+
$succeeded = true;
19+
$force = !! array_intersect(['--force', '-f'], $options);
20+
21+
foreach ($enums as $enum) {
22+
$succeeded = enumOutcome($enum, fn() => (new TypeScript($enum))->sync($force)) && $succeeded;
23+
}
24+
25+
return $succeeded;

composer.json

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,13 +27,20 @@
2727
"autoload": {
2828
"psr-4": {
2929
"Cerbero\\Enum\\": "src"
30-
}
30+
},
31+
"files": [
32+
"helpers/core.php",
33+
"helpers/cli.php"
34+
]
3135
},
3236
"autoload-dev": {
3337
"psr-4": {
34-
"Cerbero\\Enum\\": "tests"
38+
"Cerbero\\Enum\\": "tests",
39+
"App\\": "tests/Skeleton/app",
40+
"Domain\\": "tests/Skeleton/domain"
3541
}
3642
},
43+
"bin": ["bin/enum"],
3744
"scripts": {
3845
"fix": "duster fix -u tlint,phpcodesniffer,pint",
3946
"lint": "duster lint -u tlint,phpcodesniffer,pint,phpstan",

helpers/cli.php

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Cerbero\Enum;
6+
7+
use Closure;
8+
use Throwable;
9+
10+
/**
11+
* Print out the given success message.
12+
*/
13+
function succeed(string $message): bool
14+
{
15+
fwrite(STDOUT, "\e[38;2;38;220;38m{$message}\e[0m" . PHP_EOL);
16+
17+
return true;
18+
}
19+
20+
/**
21+
* Print out the given error message.
22+
*/
23+
function fail(string $message): bool
24+
{
25+
fwrite(STDERR, "\e[38;2;220;38;38m{$message}\e[0m" . PHP_EOL);
26+
27+
return false;
28+
}
29+
30+
/**
31+
* Split the given argv into arguments and options.
32+
*
33+
* @param string[] $argv
34+
* @return list<string[]>
35+
*/
36+
function splitArgv(array $argv): array
37+
{
38+
$arguments = $options = [];
39+
40+
foreach (array_slice($argv, 2) as $item) {
41+
if (str_starts_with($item, '-')) {
42+
$options[] = $item;
43+
} else {
44+
$arguments[] = $item;
45+
}
46+
}
47+
48+
return [$arguments, $options];
49+
}
50+
51+
/**
52+
* Set enum paths from the given options.
53+
*
54+
* @param string[] $options
55+
*/
56+
function setPathsByOptions(array $options): void
57+
{
58+
if ($basePath = option('base-path', $options)) {
59+
Enums::setBasePath($basePath);
60+
}
61+
62+
if ($paths = option('paths', $options)) {
63+
Enums::setPaths(...explode(',', $paths));
64+
}
65+
}
66+
67+
/**
68+
* Retrieve the value of the given option.
69+
*
70+
* @param string[] $options
71+
*/
72+
function option(string $name, array $options): ?string
73+
{
74+
$prefix = "--{$name}=";
75+
76+
foreach ($options as $option) {
77+
if (str_starts_with($option, $prefix)) {
78+
$segments = explode('=', $option, limit: 2);
79+
80+
return $segments[1] === '' ? null : $segments[1];
81+
}
82+
}
83+
84+
return null;
85+
}
86+
87+
/**
88+
* Retrieve the normalized namespaces of the given enums.
89+
*
90+
* @param list<string> $enums
91+
* @return list<class-string<\UnitEnum>>
92+
*/
93+
function normalizeEnums(array $enums): array
94+
{
95+
$namespaces = array_map(fn(string $enum) => strtr($enum, '/', '\\'), $enums);
96+
97+
return array_unique(array_filter($namespaces, 'enum_exists'));
98+
}
99+
100+
/**
101+
* Print out the outcome of the given enum operation.
102+
*
103+
* @param class-string<\UnitEnum> $namespace
104+
* @param Closure(): bool $callback
105+
*/
106+
function enumOutcome(string $enum, Closure $callback): bool
107+
{
108+
$error = null;
109+
110+
try {
111+
$succeeded = $callback();
112+
} catch (Throwable $e) {
113+
$succeeded = false;
114+
$error = "\e[38;2;220;38;38m{$e?->getMessage()}\e[0m";
115+
}
116+
117+
if ($succeeded) {
118+
fwrite(STDOUT, "\e[48;2;163;230;53m\e[38;2;63;98;18m\e[1m DONE \e[0m {$enum}" . PHP_EOL . PHP_EOL);
119+
} else {
120+
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);
121+
}
122+
123+
return $succeeded;
124+
}
125+
126+
/**
127+
* Annotate the given enum within a new process.
128+
*
129+
* @param class-string<\UnitEnum> $enum
130+
*/
131+
function runAnnotate(string $enum, bool $force = false): bool
132+
{
133+
// Once an enum is loaded, PHP accesses it from the memory and not from the disk.
134+
// Since we are writing on the disk, the enum in memory might get out of sync.
135+
// To ensure that the annotations reflect the current content of such enum,
136+
// we spin a new process to load in memory the latest state of the enum.
137+
ob_start();
138+
139+
$succeeded = cli("annotate \"{$enum}\"" . ($force ? ' --force' : ''));
140+
141+
ob_end_clean();
142+
143+
return $succeeded;
144+
}
145+
146+
/**
147+
* Run the enum CLI in a new process.
148+
*/
149+
function cli(string $command, ?int &$status = null): bool
150+
{
151+
$cmd = vsprintf('"%s" "%s" %s 2>&1', [
152+
PHP_BINARY,
153+
path(__DIR__ . '/../bin/enum'),
154+
$command,
155+
]);
156+
157+
return passthru($cmd, $status) === null;
158+
}
159+
160+
/**
161+
* Synchronize the given enum in TypeScript within a new process.
162+
*
163+
* @param class-string<\UnitEnum> $enum
164+
*/
165+
function runTs(string $enum, bool $force = false): bool
166+
{
167+
// Once an enum is loaded, PHP accesses it from the memory and not from the disk.
168+
// Since we are writing on the disk, the enum in memory might get out of sync.
169+
// To make sure that we are synchronizing the current content of such enum,
170+
// we spin a new process to load in memory the latest state of the enum.
171+
ob_start();
172+
173+
$succeeded = cli("ts \"{$enum}\"" . ($force ? ' --force' : ''));
174+
175+
ob_end_clean();
176+
177+
return $succeeded;
178+
}

0 commit comments

Comments
 (0)