Skip to content

Commit 85ac7b7

Browse files
committed
Implement TypeScript synchronization
1 parent 93c8528 commit 85ac7b7

File tree

11 files changed

+351
-47
lines changed

11 files changed

+351
-47
lines changed

README.md

Lines changed: 79 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ composer require cerbero/enum
3939
* [🦾 Console commands](#-console-commands)
4040
* [🗒️ annotate](#%EF%B8%8F-annotate)
4141
* [🏗️ make](#%EF%B8%8F-make)
42+
* [💙 ts](#%EF%B8%8F-ts)
4243

4344
To supercharge our enums with all the features provided by this package, we can let our enums use the `Enumerates` trait:
4445

@@ -406,6 +407,24 @@ This package provides a handy binary, built to automate different tasks. To lear
406407
./vendor/bin/enum
407408
```
408409

410+
For the console commands to work properly, the application base path is automatically guessed. However, in case of issues, we can manually set it by creating an `enums.php` file in the root of our app:
411+
412+
```php
413+
<?php
414+
415+
use Cerbero\Enum\Enums;
416+
417+
Enums::setBasePath(__DIR__);
418+
```
419+
420+
Some commands support the option `--all` to reference all the enums of our application. We can set the paths where enums live in our app in the `enums.php` configuration file as well:
421+
422+
```php
423+
Enums::setPaths('app/Enums', 'domain/*/Enums');
424+
```
425+
426+
In the above example, enums are discovered in the `app/Enums` directory and in all `Enums` sub-folders belonging to `domain`, e.g. `domain/Posts/Enums`, `domain/Users/Enums`, etc.
427+
409428
#### 🗒️ annotate
410429

411430
The `annotate` command automatically adds method annotations to enums, making IDEs autocompletion possible:
@@ -432,28 +451,12 @@ Otherwise we can annotate all our enums at once by enabling the option `--all`:
432451
./vendor/bin/enum annotate -a
433452
```
434453

435-
For the option `--all` to work, we need to set the paths where enums live in our application:
436-
437-
```php
438-
use Cerbero\Enum\Enums;
439-
440-
Enums::setPaths('app/Enums', 'domain/*/Enums');
441-
```
442-
443-
In the above example, enums are discovered in the `app/Enums` directory and in all `Enums` sub-folders belonging to `domain`, e.g. `domain/Posts/Enums`, `domain/Users/Enums`, etc.
444-
445-
This package tries to automatically find the application base path. However if enums can't be discovered after setting their paths, we can manually set our application base path:
446-
447-
```php
448-
Enums::setBasePath(__DIR__ . '/path/to/our/app');
449-
```
450-
451454
If we want to overwrite method annotations already annotated on enums, we can add the option `--force`:
452455

453456
```bash
454-
php artisan enum:annotate App/Enums/Enum --force
457+
./vendor/bin/enum annotate App/Enums/Enum --force
455458

456-
php artisan enum:annotate App/Enums/Enum -f
459+
./vendor/bin/enum annotate App/Enums/Enum -f
457460
```
458461

459462
#### 🏗️ make
@@ -497,10 +500,66 @@ php artisan enum:make App/Enums/Enum CaseOne CaseTwo --force
497500
php artisan enum:make App/Enums/Enum CaseOne CaseTwo -f
498501
```
499502

500-
This package tries to automatically find the application base path. However if enums can't be successfully created, we can manually set our application base path:
503+
Finally, we can generate the TypeScript counterpart of the newly created enum by adding the `--typescript` option:
504+
505+
```bash
506+
php artisan enum:make App/Enums/Enum CaseOne CaseTwo --typescript
507+
508+
php artisan enum:make App/Enums/Enum CaseOne CaseTwo -t
509+
```
510+
511+
#### 💙 ts
512+
513+
The `ts` command turns enums into their TypeScript counterpart, synchronizing backend with frontend:
514+
515+
```bash
516+
./vendor/bin/enum ts App/Enums/Enum
517+
518+
./vendor/bin/enum ts "App\Enums\Enum"
519+
```
520+
521+
We can provide more than one enum to synchronize in TypeScript, if needed:
522+
523+
```bash
524+
./vendor/bin/enum ts App/Enums/Enum1 App/Enums/Enum2
525+
526+
./vendor/bin/enum ts "App\Enums\Enum1" "App\Enums\Enum2"
527+
```
528+
529+
Otherwise we can synchronize all our enums at once by enabling the option `--all`:
530+
531+
```bash
532+
./vendor/bin/enum ts --all
533+
534+
./vendor/bin/enum ts -a
535+
```
536+
537+
By default enums are synchronized in `resources/js/enums/index.ts`, however we can customize it in our `enums.php` configuration file:
501538

502539
```php
503-
Enums::setBasePath(__DIR__ . '/path/to/our/app');
540+
<?php
541+
542+
use Cerbero\Enum\Enums;
543+
544+
// custom static path
545+
Enums::setTypeScript('frontend/enums/index.ts');
546+
547+
// custom dynamic path
548+
Enums::setTypeScript(function (string $enum) {
549+
$domain = explode('\\', $enum)[1];
550+
551+
return "resources/js/modules/{$domain}/enums.ts";
552+
});
553+
```
554+
555+
As seen above, we can either set a static path for our TypeScript enums or dynamically set the TypeScript path of an enum depending on its namespace.
556+
557+
If we want to update previously synchronized enums, we can add the option `--force`:
558+
559+
```bash
560+
./vendor/bin/enum ts App/Enums/Enum --force
561+
562+
./vendor/bin/enum ts App/Enums/Enum -f
504563
```
505564

506565
## 📆 Change log

bin/enum

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use function Cerbero\Enum\splitArgv;
99

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

1314
if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))) {
1415
try {
@@ -24,5 +25,3 @@ if (is_file($command = path(__DIR__ . '/../cli/' . ($argv[1] ?? null) . '.php'))
2425
}
2526

2627
require path(__DIR__ . '/../cli/help');
27-
28-
?>

cli/help

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Available options:
2626
--backed=VALUE How cases should be backed. VALUE is either:
2727
snake|camel|kebab|upper|lower|int0|int1|bitwise
2828
-f, --force Whether the existing enum should be overwritten
29+
-t, --typescript Whether the enum should be synced in TypeScript
2930

3031
Examples:
3132
enum make App/Enums/MyEnum Case1 Case2
@@ -34,3 +35,23 @@ Examples:
3435
enum make App/Enums/MyEnum Case1 Case2 --backed=int1
3536
enum make App/Enums/MyEnum Case1 Case2 --force
3637
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: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use function Cerbero\Enum\fail;
1010
use function Cerbero\Enum\option;
1111
use function Cerbero\Enum\runAnnotate;
12+
use function Cerbero\Enum\runTs;
1213
use function Cerbero\Enum\succeed;
1314

1415
if (! $enum = strtr($arguments[0] ?? '', '/', '\\')) {
@@ -31,4 +32,10 @@
3132
return fail('The option --backed supports only ' . implode(', ', Backed::names()));
3233
}
3334

34-
return enumOutcome($enum, fn() => $generator->generate($force) && runAnnotate($enum, $force));
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;

helpers/cli.php

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -148,13 +148,31 @@ function runAnnotate(string $enum, bool $force = false): bool
148148
*/
149149
function cli(string $command, ?int &$status = null): bool
150150
{
151-
$cmd = vsprintf('"%s" "%s" %s --base-path="%s" --paths="%s" 2>&1', [
151+
$cmd = vsprintf('"%s" "%s" %s 2>&1', [
152152
PHP_BINARY,
153153
path(__DIR__ . '/../bin/enum'),
154154
$command,
155-
Enums::basePath(),
156-
implode(',', Enums::paths()),
157155
]);
158156

159157
return passthru($cmd, $status) === null;
160158
}
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+
}

helpers/core.php

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,15 @@ function path(string $path): string
187187

188188
return $head . implode(DIRECTORY_SEPARATOR, $segments);
189189
}
190+
191+
/**
192+
* Create the directory for the given path if missing.
193+
*/
194+
function ensureParentDirectory(string $path): bool
195+
{
196+
if (file_exists($directory = dirname($path))) {
197+
return true;
198+
}
199+
200+
return mkdir($directory, 0755, recursive: true);
201+
}

src/Enums.php

Lines changed: 47 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ class Enums
2828
*/
2929
protected static array $paths = [];
3030

31+
/**
32+
* The TypeScript path to sync enums in.
33+
*
34+
* @var Closure(class-string<UnitEnum>|string $enum): string|string
35+
*/
36+
protected static Closure|string $typeScript = 'resources/js/enums/index.ts';
37+
3138
/**
3239
* The logic to run when an inaccessible enum method is called.
3340
*
@@ -85,6 +92,28 @@ public static function paths(): array
8592
return static::$paths;
8693
}
8794

95+
/**
96+
* Set the TypeScript path to sync enums in.
97+
*
98+
* @param callable(class-string<UnitEnum>|string $enum): string|string $path
99+
*/
100+
public static function setTypeScript(callable|string $path): void
101+
{
102+
/** @phpstan-ignore assign.propertyType */
103+
static::$typeScript = is_callable($path) ? $path(...) : $path;
104+
}
105+
106+
/**
107+
* Retrieve the TypeScript path, optionally for the given enum.
108+
*
109+
* @param class-string<UnitEnum>|string $enum
110+
* @return string
111+
*/
112+
public static function typeScript(string $enum = ''): string
113+
{
114+
return static::$typeScript instanceof Closure ? (static::$typeScript)($enum) : static::$typeScript;
115+
}
116+
88117
/**
89118
* Yield the namespaces of all the application enums.
90119
*
@@ -127,46 +156,46 @@ public static function onStaticCall(callable $callback): void
127156
}
128157

129158
/**
130-
* Set the logic to run when an inaccessible case method is called.
159+
* Handle the call to an inaccessible enum method.
131160
*
132-
* @param callable(UnitEnum $case, string $name, array<array-key, mixed> $arguments): mixed $callback
161+
* @param class-string<UnitEnum> $enum
162+
* @param array<array-key, mixed> $arguments
133163
*/
134-
public static function onCall(callable $callback): void
164+
public static function handleStaticCall(string $enum, string $name, array $arguments): mixed
135165
{
136-
static::$onCall = $callback(...);
166+
return static::$onStaticCall
167+
? (static::$onStaticCall)($enum, $name, $arguments)
168+
: $enum::fromName($name)->value(); /** @phpstan-ignore method.nonObject */
137169
}
138170

139171
/**
140-
* Set the logic to run when a case is invoked.
172+
* Set the logic to run when an inaccessible case method is called.
141173
*
142-
* @param callable(UnitEnum $case, mixed ...$arguments): mixed $callback
174+
* @param callable(UnitEnum $case, string $name, array<array-key, mixed> $arguments): mixed $callback
143175
*/
144-
public static function onInvoke(callable $callback): void
176+
public static function onCall(callable $callback): void
145177
{
146-
static::$onInvoke = $callback(...);
178+
static::$onCall = $callback(...);
147179
}
148180

149181
/**
150-
* Handle the call to an inaccessible enum method.
182+
* Handle the call to an inaccessible case method.
151183
*
152-
* @param class-string<UnitEnum> $enum
153184
* @param array<array-key, mixed> $arguments
154185
*/
155-
public static function handleStaticCall(string $enum, string $name, array $arguments): mixed
186+
public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed
156187
{
157-
return static::$onStaticCall
158-
? (static::$onStaticCall)($enum, $name, $arguments)
159-
: $enum::fromName($name)->value(); /** @phpstan-ignore method.nonObject */
188+
return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name);
160189
}
161190

162191
/**
163-
* Handle the call to an inaccessible case method.
192+
* Set the logic to run when a case is invoked.
164193
*
165-
* @param array<array-key, mixed> $arguments
194+
* @param callable(UnitEnum $case, mixed ...$arguments): mixed $callback
166195
*/
167-
public static function handleCall(UnitEnum $case, string $name, array $arguments): mixed
196+
public static function onInvoke(callable $callback): void
168197
{
169-
return static::$onCall ? (static::$onCall)($case, $name, $arguments) : $case->resolveMetaAttribute($name);
198+
static::$onInvoke = $callback(...);
170199
}
171200

172201
/**

0 commit comments

Comments
 (0)