diff --git a/src/Enum/JavascriptConverter.php b/src/Enum/JavascriptConverter.php
new file mode 100644
index 0000000..ce114e3
--- /dev/null
+++ b/src/Enum/JavascriptConverter.php
@@ -0,0 +1,286 @@
+useSymbol,
+ $this->useImmutability,
+ $casing,
+ $this->indentSize,
+ $this->export,
+ );
+ }
+
+ public function useImmutability(): self
+ {
+ return match ($this->useImmutability) {
+ true => $this,
+ default => new self(
+ $this->useSymbol,
+ true,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ $this->export,
+ ),
+ };
+ }
+
+ public function ignoreImmutability(): self
+ {
+ return match ($this->useImmutability) {
+ false => $this,
+ default => new self(
+ $this->useSymbol,
+ false,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ $this->export,
+ ),
+ };
+ }
+
+ public function useSymbol(): self
+ {
+ return match ($this->useSymbol) {
+ true => $this,
+ default => new self(
+ true,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ $this->export,
+ ),
+ };
+ }
+
+ public function ignoreSymbol(): self
+ {
+ return match ($this->useSymbol) {
+ false => $this,
+ default => new self(
+ false,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ $this->export,
+ ),
+ };
+ }
+
+ public function useExportDefault(): self
+ {
+ return match ($this->export) {
+ self::EXPORT_DEFAULT => $this,
+ default => new self(
+ $this->useSymbol,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ self::EXPORT_DEFAULT,
+ ),
+ };
+ }
+
+ public function useExport(): self
+ {
+ return match ($this->export) {
+ self::EXPORT_SIMPLE => $this,
+ default => new self(
+ $this->useSymbol,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ self::EXPORT_SIMPLE,
+ ),
+ };
+ }
+
+ public function ignoreExport(): self
+ {
+ return match ($this->export) {
+ self::EXPORT_NONE => $this,
+ default => new self(
+ $this->useSymbol,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $this->indentSize,
+ self::EXPORT_NONE,
+ ),
+ };
+ }
+
+ public function intendSize(int $indentSize): self
+ {
+ return match (true) {
+ $indentSize < 0 => throw new ValueError('indentation size can no be negative.'),
+ $indentSize === $this->indentSize => $this,
+ default => new self(
+ $this->useSymbol,
+ $this->useImmutability,
+ $this->propertyNameCasing,
+ $indentSize,
+ $this->export,
+ ),
+ };
+ }
+
+ /**
+ * Converts the Enum into a Javascript object.
+ *
+ *
+ * - If the object name is null the object is not assign to const variable
+ * - If the object name is the empty string the PHP namespaced class name will be used
+ * - If the object name is a non-empty string, it will be used as is as the const variable name
+ *
+ *
+ * @param class-string $enumClass
+ */
+ 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.'}';
+ if ($this->useImmutability) {
+ $output = "Object.freeze($output)";
+ }
+
+ $objectName = $this->sanitizeName($objectName, $enumClass);
+ if (null !== $objectName) {
+ $output = "const $objectName = $output";
+ if (self::EXPORT_DEFAULT === $this->export) {
+ return $output.';'.$eol.$this->export.$objectName.';'.$eol;
+ }
+ }
+
+ return $this->export.$output.$eol;
+ }
+
+ /**
+ * Converts the Enum into a Javascript class.
+ *
+ *
+ * - If the class name is the empty string the PHP namespaced class name will be used
+ * - If the class name is a non-empty string, it will be used as is as the class name
+ *
+ *
+ * @param class-string $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";
+ }
+
+ $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.'}';
+
+ return $this->export.$output.$eol;
+ ;
+ }
+
+ /**
+ * @param class-string $enumClass
+ *
+ * @throws ValueError If the given string does not represent a Backed Enum class
+ */
+ 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
+ {
+ if ('' !== $className) {
+ return $className;
+ }
+
+ $parts = explode('\\', $enumClass);
+
+ return (string) array_pop($parts);
+ }
+
+ private function formatPropertyName(BackedEnum $enum): string
+ {
+ return match ($this->propertyNameCasing) {
+ null => $enum->name,
+ default => ($this->propertyNameCasing)($enum->name),
+ };
+ }
+
+ private function formatPropertyValue(BackedEnum $enum): string|int
+ {
+ $value = $enum->value;
+ $value = is_string($value) ? '"'.$value.'"' : $value;
+
+ return match ($this->useSymbol) {
+ true => 'Symbol('.$value.')',
+ default => $value,
+ };
+ }
+}
diff --git a/src/Enum/JavascriptConverterTest.php b/src/Enum/JavascriptConverterTest.php
new file mode 100644
index 0000000..21346ff
--- /dev/null
+++ b/src/Enum/JavascriptConverterTest.php
@@ -0,0 +1,204 @@
+expectException(ValueError::class);
+
+ JavascriptConverter::new()->intendSize(-1);
+ }
+
+ #[Test]
+ public function it_will_fails_converting_a_non_backed_enum(): void
+ {
+ $this->expectException(ValueError::class);
+
+ JavascriptConverter::new()->convertToObject(HttpMethod::class); /* @phpstan-ignore-line */
+ }
+
+ #[Test]
+ public function it_will_fails_converting_a_non_enum(): void
+ {
+ $this->expectException(ValueError::class);
+
+ JavascriptConverter::new()->convertToObject(self::class); /* @phpstan-ignore-line */
+ }
+
+ #[Test]
+ public function it_will_convert_to_a_javascript_immutable_object_by_default(): void
+ {
+ $expected = <<useImmutability()
+ ->ignoreExport()
+ ->ignoreSymbol()
+ ->intendSize(2);
+
+ self::assertSame($expected, $converter->convertToObject(HttpStatusCode::class));
+ self::assertSame($expected, $altConverter->convertToObject(HttpStatusCode::class));
+ }
+
+ #[Test]
+ public function it_can_convert_to_a_javascript_mutable_object(): void
+ {
+ $expected = <<ignoreImmutability()
+ ->propertyNameCase(strtolower(...))
+ ->useSymbol()
+ ->useExport()
+ ->intendSize(4)
+ ->convertToObject(HttpStatusCode::class, 'Foobar')
+ );
+ }
+
+ #[Test]
+ public function it_will_convert_to_a_javascript_class_by_default(): void
+ {
+ $expected = <<useImmutability()
+ ->ignoreExport()
+ ->ignoreSymbol()
+ ->intendSize(2);
+
+ self::assertSame($expected, $converter->convertToClass(HttpStatusCode::class));
+ self::assertSame($expected, $altConverter->convertToClass(HttpStatusCode::class));
+ }
+
+ #[Test]
+ public function it_can_convert_to_a_javascript_class(): void
+ {
+ $pascalCase = fn (string $word): string => implode('', array_map(
+ ucfirst(...),
+ explode(
+ ' ',
+ strtolower(str_replace(['_', '-'], [' ', ' '], $word))
+ )
+ ));
+
+ $expected = <<useImmutability()
+ ->ignoreSymbol()
+ ->intendSize(4)
+ ->useExportDefault()
+ ->propertyNameCase(fn (string $name) => $pascalCase(strtolower(str_replace('HTTP_', '', $name))));
+
+ self::assertSame(
+ $expected,
+ $converter->convertToClass(HttpStatusCode::class, 'Foobar')
+ );
+ }
+
+ #[Test]
+ public function it_can_convert_to_a_javascript_object_with_export_default_and_a_variable_name(): void
+ {
+ $pascalCase = fn (string $word): string => implode('', array_map(
+ ucfirst(...),
+ explode(
+ ' ',
+ strtolower(str_replace(['_', '-'], [' ', ' '], $word))
+ )
+ ));
+
+ $expected = <<useImmutability()
+ ->useExportDefault()
+ ->useSymbol()
+ ->intendSize(4)
+ ->propertyNameCase(fn (string $name) => $pascalCase(strtolower(str_replace('HTTP_', '', $name))))
+ ->convertToObject(HttpStatusCode::class, 'StatusCode');
+
+ self::assertSame($expected, $actual);
+ }
+
+ #[Test]
+ public function it_can_convert_to_a_javascript_object_with_export_default_and_no_variable(): void
+ {
+ $pascalCase = fn (string $word): string => implode('', array_map(
+ ucfirst(...),
+ explode(
+ ' ',
+ strtolower(str_replace(['_', '-'], [' ', ' '], $word))
+ )
+ ));
+
+ $expected = <<useImmutability()
+ ->useExportDefault()
+ ->useSymbol()
+ ->intendSize(0)
+ ->propertyNameCase(fn (string $name) => $pascalCase(strtolower(str_replace('HTTP_', '', $name))))
+ ->convertToObject(HttpStatusCode::class);
+
+ self::assertSame($expected, $actual);
+ }
+}
diff --git a/src/Enum/README.md b/src/Enum/README.md
index 4c5473b..aa85a8a 100644
--- a/src/Enum/README.md
+++ b/src/Enum/README.md
@@ -193,6 +193,109 @@ enum HttpMethod: string
}
```
+### Converting the Enum into a Javascript structure
+
+The `JavascriptConverter` enables converting your PHP Backed 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:
+
+```php
+enum HttpStatusCode: int
+{
+ case HTTP_OK = 200;
+ case HTTP_REDIRECTION = 302;
+ case HTTP_NOT_FOUND = 404;
+ case HTTP_SERVER_ERROR = 500;
+}
+```
+
+It can be converted into an object using the `convertToObject` method:
+
+```php
+use Bakame\Aide\Enum\JavascriptConverter;
+
+echo JavascriptConverter::new()->convertToObject(HttpStatusCode::class);
+```
+
+will produce the following javascript code snippet:
+
+```javascript
+Object.freeze({
+ HTTP_OK: 200,
+ HTTP_REDIRECTION: 302,
+ HTTP_NOT_FOUND: 404,
+ HTTP_SERVER_ERROR: 500,
+})
+```
+
+conversely using `convertToClass` as follows:
+
+```php
+echo JavascriptConverter::new()->convertToClass(HttpStatusCode::class);
+```
+
+will produce the following javascript code snippet:
+
+```javascript
+class HttpStatusCode {
+ static HTTP_OK = new HttpStatusCode(200)
+ static HTTP_REDIRECTION = new HttpStatusCode(302)
+ static HTTP_NOT_FOUND = new HttpStatusCode(404)
+ static HTTP_SERVER_ERROR = new HttpStatusCode(500)
+
+ constructor(name) {
+ this.name = name
+ }
+}
+```
+
+Of course there are ways to improve the output depending on your use case you can
+
+- ignore or use object immutability;
+- ignore or use Javascript `export` or `export default`;
+- change the class name or add and/or change the object variable name;
+- use `Symbol` when declaring the object property value;
+- define indentation spaces and thus end of line;
+
+Here's a more advance usage of the converter to highlight how you can configure it.
+
+```php
+useImmutability()
+ ->useExportDefault()
+ ->useSymbol()
+ ->intendSize(4)
+ ->propertyNameCase(
+ fn (string $name) => Str::of($name)->replace('HTTP_', '')->lower()->studly()->toString()
+ );
+
+echo $converter->convertToObject(HttpStatusCode::class, 'StatusCode');
+```
+
+will return the following Javascript code:
+
+```javascript
+const StatusCode = Object.freeze({
+ Ok: Symbol(200),
+ Redirection: Symbol(302),
+ NotFound: Symbol(404),
+ ServerError: Symbol(500),
+});
+export default StatusCode;
+```
+
+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:
+
+- using vanilla PHP with `file_put_contents` or `SplFileObject`
+- using more robust and battle tested packages you can find on packagist for instance.
+
## Credits
- [ignace nyamagana butera](https://github.com/nyamsprod)