Skip to content

Commit

Permalink
Add support for Pure PHP Enum
Browse files Browse the repository at this point in the history
  • Loading branch information
nyamsprod committed Jan 21, 2024
1 parent db8b20e commit 214f77c
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 40 deletions.
109 changes: 76 additions & 33 deletions src/Enum/JavascriptConverter.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use BackedEnum;
use Closure;
use ReflectionEnum;
use UnitEnum;
use ValueError;

final class JavascriptConverter
Expand All @@ -21,6 +21,7 @@ private function __construct(
private readonly ?Closure $propertyNameCasing,
private readonly int $indentSize,
private readonly string $export,
private readonly int $unitEnumStartAt
) {
}

Expand All @@ -31,7 +32,8 @@ public static function new(): self
useImmutability: true,
propertyNameCasing: null,
indentSize: 2,
export: self::EXPORT_NONE
export: self::EXPORT_NONE,
unitEnumStartAt: 0,
);
}

Expand All @@ -43,6 +45,7 @@ public function propertyNameCase(Closure $casing = null): self
$casing,
$this->indentSize,
$this->export,
$this->unitEnumStartAt,
);
}

Expand All @@ -56,6 +59,7 @@ public function useImmutability(): self
$this->propertyNameCasing,
$this->indentSize,
$this->export,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -70,6 +74,7 @@ public function ignoreImmutability(): self
$this->propertyNameCasing,
$this->indentSize,
$this->export,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -84,6 +89,7 @@ public function useSymbol(): self
$this->propertyNameCasing,
$this->indentSize,
$this->export,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -98,6 +104,7 @@ public function ignoreSymbol(): self
$this->propertyNameCasing,
$this->indentSize,
$this->export,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -112,6 +119,7 @@ public function useExportDefault(): self
$this->propertyNameCasing,
$this->indentSize,
self::EXPORT_DEFAULT,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -126,6 +134,7 @@ public function useExport(): self
$this->propertyNameCasing,
$this->indentSize,
self::EXPORT,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -140,6 +149,22 @@ public function ignoreExport(): self
$this->propertyNameCasing,
$this->indentSize,
self::EXPORT_NONE,
$this->unitEnumStartAt,
),
};
}

public function startAt(int $startAt): self
{
return match (true) {
$startAt === $this->unitEnumStartAt => $this,
default => new self(
$this->useSymbol,
$this->useImmutability,
$this->propertyNameCasing,
$this->indentSize,
$this->export,
$startAt,
),
};
}
Expand All @@ -155,6 +180,7 @@ public function indentSize(int $indentSize): self
$this->propertyNameCasing,
$indentSize,
$this->export,
$this->unitEnumStartAt,
),
};
}
Expand All @@ -172,22 +198,15 @@ public function indentSize(int $indentSize): self
*/
public function convertToObject(string $enumClass, ?string $objectName = null): string
{
$this->filterBackedEnum($enumClass);

$space = '';
$eol = '';
if (0 < $this->indentSize) {
$space = str_repeat(' ', $this->indentSize);
$eol = "\n";
}

$output = array_reduce(
$enumClass::cases(),
fn (string $output, BackedEnum $enum): string => $output.$space.$this->formatPropertyName($enum).': '.$this->formatPropertyValue($enum).','.$eol,
''
);

$output = '{'.$eol.$output.'}';
$body = $this->getObjectBody($enumClass, $space, $eol);
$output = '{'.$eol.$body.'}';
if ($this->useImmutability) {
$output = "Object.freeze($output)";
}
Expand All @@ -203,6 +222,21 @@ public function convertToObject(string $enumClass, ?string $objectName = null):
return $this->export.$output.$eol;
}

/**
* @param class-string<UnitEnum> $enumClass
*/
private function getObjectBody(string $enumClass, string $space, string $eol): string
{
$this->filterBackedEnum($enumClass);

$output = [];
foreach ($enumClass::cases() as $offset => $enum) {
$output[] = $space.$this->formatPropertyName($enum).': '.$this->formatPropertyValue($enum, $offset).',';
}

return implode($eol, $output).$eol;
}

/**
* Converts the Enum into a Javascript class.
*
Expand All @@ -211,33 +245,43 @@ public function convertToObject(string $enumClass, ?string $objectName = null):
* <li>If the class name is a non-empty string, it will be used as is as the class name</li>
* </ul>
*
* @param class-string<BackedEnum> $enumClass
* @param class-string<UnitEnum> $enumClass
*/
public function convertToClass(string $enumClass, string $className = ''): string
{
$this->filterBackedEnum($enumClass);

$space = '';
$eol = '';
if (0 < $this->indentSize) {
$space = str_repeat(' ', $this->indentSize);
$eol = "\n";
}

/** @var string $className */
$className = $this->sanitizeName($className, $enumClass);
$output = array_reduce(
$enumClass::cases(),
fn (string $output, BackedEnum $enum): string => $output.$space."static {$this->formatPropertyName($enum)} = new $className({$this->formatPropertyValue($enum)})$eol",
''
);

$output = 'class '.$className.' {'.$eol.$output.$eol.$space.'constructor(name) {'.$eol.$space.$space.'this.name = name'.$eol.$space.'}'.$eol.'}';
$body = $this->getClassBody($enumClass, $className, $space, $eol);
$output = 'class '.$className.' {'.$eol.$body.$eol.$space.'constructor(name) {'.$eol.$space.$space.'this.name = name'.$eol.$space.'}'.$eol.'}';

return $this->export.$output.$eol;
}

/**
* @param class-string<BackedEnum> $enumClass
* @param class-string<UnitEnum> $enumClass
*/
private function getClassBody(string $enumClass, string $className, string $space, string $eol): string
{
$this->filterBackedEnum($enumClass);

$output = [];
foreach ($enumClass::cases() as $offset => $enum) {
$output[] = $space."static {$this->formatPropertyName($enum)} = new $className({$this->formatPropertyValue($enum, $offset)})";
}

return implode($eol, $output).$eol;

}

/**
* @param class-string<UnitEnum> $enumClass
*
* @throws ValueError If the given string does not represent a Backed Enum class
*/
Expand All @@ -246,11 +290,6 @@ private function filterBackedEnum(string $enumClass): void
if (!enum_exists($enumClass)) {
throw new ValueError($enumClass.' is not a valid PHP Enum.');
}

$reflection = new ReflectionEnum($enumClass);
if (!$reflection->isBacked()) {
throw new ValueError($enumClass.' is not a PHP backed enum.');
}
}

private function sanitizeName(?string $className, string $enumClass): ?string
Expand All @@ -264,22 +303,26 @@ private function sanitizeName(?string $className, string $enumClass): ?string
return (string) array_pop($parts);
}

private function formatPropertyName(BackedEnum $enum): string
private function formatPropertyName(UnitEnum $enum): string
{
return match ($this->propertyNameCasing) {
null => $enum->name,
default => ($this->propertyNameCasing)($enum->name),
};
}

private function formatPropertyValue(BackedEnum $enum): string|int
private function formatPropertyValue(UnitEnum $enum, int $offset = 0): string|int
{
$value = $enum->value;
$isBackedEnum = $enum instanceof BackedEnum;
$value = $isBackedEnum ? $enum->value : $offset + $this->unitEnumStartAt;
$value = is_string($value) ? '"'.$value.'"' : $value;

return match ($this->useSymbol) {
true => 'Symbol('.$value.')',
default => $value,
return match ($isBackedEnum) {
true => match ($this->useSymbol) {
true => 'Symbol('.$value.')',
default => $value,
},
false => 'Symbol('.$value.')',
};
}
}
4 changes: 1 addition & 3 deletions src/Enum/JavascriptConverterTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ public function it_will_fails_with_a_negative_indent_size(): void
#[Test]
public function it_will_fails_converting_a_non_backed_enum(): void
{
$this->expectException(ValueError::class);

JavascriptConverter::new()->convertToObject(HttpMethod::class); /* @phpstan-ignore-line */
$actual = JavascriptConverter::new()->convertToObject(HttpMethod::class); /* @phpstan-ignore-line */
}

#[Test]
Expand Down
76 changes: 72 additions & 4 deletions src/Enum/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,11 +195,16 @@ enum HttpMethod: string

### Converting the Enum into a Javascript structure

The `JavascriptConverter` enables converting your PHP Backed Enum into an equivalent structure in Javascript.
The `JavascriptConverter` enables converting your PHP Enum into an equivalent structure in Javascript.
Because there are two (2) ways to create an Enum like structure in Javascript, the class provides
two (2) methods, `convertToObject` and `convertToClass` to allow the conversion. In both cases,
the conversion is configurable via wither methods to control the formatting and the
Javascript structure properties. For instance, given I have the following enum:
two (2) methods to allow the conversion.

In both cases, the conversion is configurable via wither methods to control the formatting and the
Javascript structure properties.

#### Backed Enum

For instance, given I have the following enum:

```php
enum HttpStatusCode: int
Expand Down Expand Up @@ -290,6 +295,69 @@ const StatusCode = Object.freeze({
export default StatusCode;
```

#### Pure Enum

For Pure PHP Enum, the converter will assign a unique `Symbol` value for each case, starting
wih the `Symbol(0)` and following the PHP order of case declaration. you can optionally
configure the start value using the `startAt` method.

Let's take the following PHP Pure Enum:

```php
enum Color
{
case Red;
case Blue;
case Green;
}
```

It can be converted into an object using the `convertToObject` method:

```php
use Bakame\Aide\Enum\JavascriptConverter;

echo JavascriptConverter::new()->convertToObject(Color::class);
```

will produce the following javascript code snippet:

```javascript
Object.freeze({
Red: Symbol(0),
Blue: Symbol(1),
Green: Symbol(2),
})
```

If you set up the starting value to increment you will get a different value:

```php
use Bakame\Aide\Enum\JavascriptConverter;

echo JavascriptConverter::new()
->ignoreSymbol()
->startAt(2)
->convertToClass(Color::class);
```

Then the start at value will be taken into account as shown below:

```javascript
class Color {
static Red = new Color(Symbol(2))
static Blue = new Color(Symbol(3))
static Green = new Color(Symbol(4))

constructor(name) {
this.name = name
}
}
```

> [!CAUTION]
> For Pure Enum the `ignoreSymbol` and `useSymbol` methods have no effect on the output.
The converter will not store the resulting string into a Javascriot file as this part is
left to the discretion of the implementor. There are several ways to do so:

Expand Down

0 comments on commit 214f77c

Please sign in to comment.