riverwaysoft/php-converter

PHP DTO converter to TypeScript / Dart

0.7.9.10 2023-03-24 08:29 UTC

README

Screen Shot 2022-10-07 at 09 04 35

Generates TypeScript & Dart out of your PHP DTO classes.

Why?

Statically typed languages like TypeScript or Dart are great because they allow catching bugs without even running your code. But unless you have well-defined contracts between API and consumer apps, you have to always fix outdated typings when the API changes. This library generates types for you so you can move faster and encounter fewer bugs.

Requirements

PHP 8.0 or above

Quick start

  1. Installation
composer require riverwaysoft/php-converter --dev
  1. Mark a few classes with #[Dto] annotation to convert them into TypeScript or Dart
use Riverwaysoft\DtoConverter\ClassFilter\Dto;

#[Dto]
class UserOutput
{
    public string $id;
    public int $age;
    public ?UserOutput $bestFriend;
    /** @var UserOutput[] */
    public array $friends;
}
  1. Run CLI command to generate TypeScript
vendor/bin/dto-converter-ts generate --from=/path/to/project/src --to=.

You'll get file generated.ts with the following contents:

type UserOutput = {
  id: string;
  age: number;
  bestFriend: UserOutput | null;
  friends: UserOutput[];
}

Features

  • Support of all PHP data types including union types, nullable types, enums (both native PHP 8.1 enums and MyCLabs enums)
  • PHP DocBlock type support e.g User[], int[][]|null
  • Nested types
  • Recursive types
  • Custom type resolvers (for example for DateTimeImmutable, etc)
  • Generate a single output file or multiple files (1 type per file). An option to override the generation logic
  • Flexible class filters with an option to use your own filters

Customize

If you'd like to customize dto-converter-ts you need to copy the generator script to your project folder:

cp vendor/bin/dto-converter-ts bin/dto-converter-ts

Now you can start customizing the dto-converter by editing the executable file.

How to customize generated output?

By default dto-converter writes all the types into one file. You can configure it to put each type / class in a separate file with all the required imports. Here is an example how to achieve it:

+ $fileNameGenerator = new KebabCaseFileNameGenerator('.ts');

$application->add(
    new ConvertCommand(
        new Converter(new PhpAttributeFilter('Dto')),
        new TypeScriptGenerator(
-            new SingleFileOutputWriter('generated.ts'),
+            new EntityPerClassOutputWriter(
+                $fileNameGenerator,
+                new TypeScriptImportGenerator(
+                    $fileNameGenerator,
+                    new DtoTypeDependencyCalculator()
+                )
+            ),
            [
                new DateTimeTypeResolver(),
                new ClassNameTypeResolver(),
            ],
        ),
        new Filesystem(),
        new OutputDiffCalculator(),
        new FileSystemCodeProvider('/\.php$/'),
    )
);

Feel free to create your own OutputWriter.

How to customize class filtering?

Suppose you don't want to mark each DTO individually with #[Dto] but want to convert all the files ending with "Dto" automatically:

$application->add(
    new ConvertCommand(
-       new Converter(new PhpAttributeFilter('Dto')),
+       new Converter(),
        new TypeScriptGenerator(
            new SingleFileOutputWriter('generated.ts'),
            [
                new DateTimeTypeResolver(),
                new ClassNameTypeResolver(),
            ],
        ),
        new Filesystem(),
        new OutputDiffCalculator(),
-       new FileSystemCodeProvider('/\.php$/'),
+       new FileSystemCodeProvider('/Dto\.php$/'),
    )
);

You can even go further and use NegationFilter to exclude specific files as shown in unit tests.

How to write custom type resolvers?

dto-converter takes care of converting basic PHP types like number, string and so on. But what if you have a type that isn't a DTO? For example \DateTimeImmutable. You can write a class that implements UnknownTypeResolverInterface. There is also a shortcut to achieve it - use InlineTypeResolver:

+use Riverwaysoft\DtoConverter\Dto\PhpType\PhpBaseType;

$application->add(
    new ConvertCommand(
        new Converter(new PhpAttributeFilter('Dto')),
        new TypeScriptGenerator(
            new SingleFileOutputWriter('generated.ts'),
            [
                new DateTimeTypeResolver(),
                new ClassNameTypeResolver(),
+               new InlineTypeResolver([
+                 // Convert libphonenumber object to a string
+                 // PhpBaseType is used to support both Dart/TypeScript
+                 'PhoneNumber' => PhpBaseType::string(), 
+                 // Convert PHP Money object to a custom TypeScript type
+                 // It's TS-only syntax, to support Dart and the rest of the languages you'd have to create a separate PHP class like MoneyOutput
+                 'Money' => '{ amount: number; currency: string }',
+                 // Convert Doctrine Embeddable to an existing Dto marked as #[Dto]
+                 'SomeDoctrineEmbeddable' => 'SomeDoctrineEmbeddableDto',
+               ])
            ],
        ),
        new Filesystem(),
        new OutputDiffCalculator(),
        new FileSystemCodeProvider('/\.php$/'),
    )
);

How to customize generated output file?

You may want to apply some transformations on the resulted file with types. For example you may want to format it with tool of your choice or prepend code with a warning like "// The file was autogenerated, don't edit it manually". To add such a warning you can already use the built-in extension:

$application->add(
    new ConvertCommand(
        new Converter(new PhpAttributeFilter('Dto')),
        new TypeScriptGenerator(
            new SingleFileOutputWriter('generated.ts'),
            [
                new DateTimeTypeResolver(),
                new ClassNameTypeResolver(),
            ],
+           new OutputFilesProcessor([
+               new PrependAutogeneratedNoticeFileProcessor(),
+           ]),
        ),
        new Filesystem(),
        new OutputDiffCalculator(),
        new FileSystemCodeProvider('/\.php$/'),
    )
);

Feel free to create your own processor based on PrependAutogeneratedNoticeFileProcessor source.

Here is an example how Prettier formatter could look like:

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

class PrettierFormatProcessor implements SingleOutputFileProcessorInterface
{
    public function process(OutputFile $outputFile): OutputFile
    {
        $fs = new Filesystem();
        $temporaryGeneratedFile = $fs->tempnam(sys_get_temp_dir(), "dto", '.ts');
        $fs->appendToFile($temporaryGeneratedFile, $outputFile->getContent());

        $process = new Process(["./node_modules/.bin/prettier", $temporaryGeneratedFile, '--write', '--config', '.prettierrc.js']);
        $process->run();

        if (!$process->isSuccessful()) {
            throw new ProcessFailedException($process);
        }

        return new OutputFile(
            relativeName: $outputFile->getRelativeName(),
            content: file_get_contents($temporaryGeneratedFile)
        );
    }
}

Then add it to the list:

$application->add(
    new ConvertCommand(
        new Converter(new PhpAttributeFilter('Dto')),
        new TypeScriptGenerator(
            new SingleFileOutputWriter('generated.ts'),
            [
                new DateTimeTypeResolver(),
                new ClassNameTypeResolver(),
            ],
+           new OutputFilesProcessor([
+               new PrependAutogeneratedNoticeFileProcessor(),
+               new PrettierFormatProcessor(),
+           ]),
        ),
        new Filesystem(),
        new OutputDiffCalculator(),
        new FileSystemCodeProvider('/\.php$/'),
    )
);

How to add support for other languages?

To write a custom converter you can implement LanguageGeneratorInterface. Here is an example how to do it for Go language: GoGeneratorSimple. Check how to use it here. It covers only basic scenarios to get you an idea, so feel free to modify it to your needs.

Error list

Here is a list of errors dto-converter can throw and description what to do if you encounter these errors:

1. Property z of class X has no type. Please add PHP type

It means that you've forgotten to add type for property a of class Y. Example:

#[Dto]
class X {
  public $z;
} 

At the moment there is no strict / loose mode in dto-converter. It is always strict. If you don't know the PHP type just use mixed type to explicitly convert it to any/Object. It could silently convert such types to TypeScript any or Dart Object if we needed it. But we prefer an explicit approach. Feel free to raise an issue if having loose mode makes sense for you.

2. PHP Type X is not supported

It means dto-converter doesn't know how to convert the type X into TypeScript or Dart. If you are using #[Dto] attribute you probably forgot to add it to class X. Example:

#[Dto]
class A {
  public X $x;
}

class X {
  public int $foo;
}

Testing

composer test

How it is different from alternatives?

  • Unlike spatie/typescript-transformer dto-converter supports not only TypeScript but also Dart. Support for other languages can be easily added by implementing LanguageInterface. dto-converter can also output generated types / classes into different files.
  • Unlike grpc dto-converter doesn't require to modify your app or install some extensions.

Contributing

Please see CONTRIBUTING for details.