Skip to content

Brainshaker95/php-to-ts-bundle

Repository files navigation

Contributors Forks Stargazers Issues MIT License

PhpToTsBundle

Convert PHP model classes to TypeScript interfaces

This project is still in a very early state
Everything is subject to change. Use at your own risk!

☄️ Bug reports / feature requests »


Table Of Contents

   ⬆   

👋 About The Project

This bundle aims to provide a simple way of working with strongly typed JSON response data.

Given a PHP class like this:

<?php

namespace App\Model\TypeScriptables;

use Brainshaker95\PhpToTsBundle\Attribute\AsTypeScriptable;

/**
 * This is a class description
 *
 * @deprecated use MyOtherClass instead
 */
#[AsTypeScriptable]
final class MyClass extends MyParentClass
{
    /**
     * @param non-empty-list<array{
     *     foo: int,
     *     bar: array{
     *         baz: Baz,
     *     },
     * }>[] $foo1 This is a promoted constructor property description
     * with a new line
     * and another one
     */
    public function __construct(
        public array $foo1,
        public readonly Foo&Bar $bar1,
        public ?bool $baz1,
    ) {}

    /**
     * @var non-empty-string|array<int,string>
     */
    public $foo2;

    /**
     * This is a property description
     * with a new line
     *
     * @deprecated
     */
    public readonly iterable $bar2;

    /**
     * @template T of 'foo'|'bar'|'baz' = 'bar'
     *
     * @var T|float|null
     */
    public string|float|null $baz2;
}

A TypeScript interface like this will be generated:

/**
 * Auto-generated by PhpToTsBundle
 * Do not modify directly!
 */

import type { Bar } from './bar';
import type { Baz } from './baz';
import type { Foo } from './foo';
import type { MyParentClass } from './my-parent-class';

/**
 * This is a class description
 *
 * @deprecated use MyOtherClass instead
 */
export interface MyClass<
  T extends ('foo' | 'bar' | 'baz') = 'bar',
> extends MyParentClass {
  readonly bar1: (Foo & Bar);
  /**
   * This is a property description
   * with a new line
   *
   * @deprecated
   */
  readonly bar2: unknown[];
  baz1: (boolean | null);
  /**
   * This is a promoted constructor property description
   * with a new line
   * and another one
   */
  foo1: Array<{
    foo: number;
    bar: {
      baz: Baz;
    };
  }>[];
  baz2: (T | number | null);
  foo2: (string | Record<number, string>);
}

   ⬆   

🚀 Installation

Currently this bundle is not available as a composer package. It can still be installed like this:

composer.json

{
  // ...
  "require": {
    // ...
    "brainshaker95/php-to-ts-bundle": "dev-master"
  },
  "repositories": [
    // ...
    {
      "type": "vcs",
      "url": "git@github.com:Brainshaker95/php-to-ts-bundle.git"
    }
  ]
}
composer update brainshaker95/php-to-ts-bundle

config/bundles.php

<?php

return [
    // ...
    Brainshaker95\PhpToTsBundle\PhpToTsBundle::class => ['dev' => true],
];

   ⬆   

⚙ Configuration

packages\php_to_ts.yaml

# Default configuration

php_to_ts:
  # Directory in which to look for models to include
  input_dir: src/Model
  
  # Directory in which to dump TypeScript interfaces
  output_dir: assets/ts/types/php-to-ts

  # File type to use for TypeScript interfaces
  file_type: !php/const Brainshaker95\PhpToTsBundle\Model\Config\FileType::TYPE_MODULE

  # Type definition type to use for TypeScript interfaces
  type_definition_type: !php/const Brainshaker95\PhpToTsBundle\Model\Config\TypeDefinitionType::TYPE_INTERFACE

  # Indentation used for TypeScript interfaces
  indent:
    # Indent style used for TypeScript interfaces
    style: !php/const Brainshaker95\PhpToTsBundle\Model\Config\Indent::STYLE_SPACE

    # Number of indent style characters per indent
    count: 2

  # Quote style used for strings in TypeScript interfaces
  quotes: !php/const Brainshaker95\PhpToTsBundle\Model\Config\Quotes::STYLE_SINGLE

  # Class names of sort strategies used for TypeScript properties
  sort_strategies: 
    - Brainshaker95\PhpToTsBundle\Model\Config\SortStrategy\AlphabeticalAsc
    - Brainshaker95\PhpToTsBundle\Model\Config\SortStrategy\ConstructorFirst
    - Brainshaker95\PhpToTsBundle\Model\Config\SortStrategy\ReadonlyFirst

  # Class name of file name strategies used for TypeScript files
  file_name_strategy: Brainshaker95\PhpToTsBundle\Model\Config\FileNameStrategy\KebabCase

   ⬆   

👀 Usage

This bundle exposes 3 different commands.
All of them use the default configuration when no options are passed.
Run bin/console <command> -h for a full list of available options.

Dumps all TypeScriptables in the given directory (input-dir: string):

bin/console phptots:dump:dir [<input-dir>] [options]

Dumps all TypeScriptables in the given files and directories (input-files: string[]):

bin/console phptots:dump:files <input-files> [options]

Dumps all TypeScriptables in the given file (input-file: string):

bin/console phptots:dump:file <input-file> [options]

   ⬆   

💻 API

🤝 Events

Each time a TsInterface, TsEnum or TsProperty instance is generated during the dumping process an event is dispatched.
You can subscribe to these events if it is necessary to modify the output right before dumping.

Example implementation:

<?php

declare(strict_types=1);

namespace App\EventSubscriber;

use Brainshaker95\PhpToTsBundle\Event\TsInterfaceGeneratedEvent;
use Brainshaker95\PhpToTsBundle\Event\TsEnumGeneratedEvent;
use Brainshaker95\PhpToTsBundle\Event\TsPropertyGeneratedEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class TsInterfaceGeneratedSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            TsInterfaceGeneratedEvent::class => 'onGeneratedTsInterface',
            TsEnumGeneratedEvent::class      => 'onGeneratedTsEnum',
            TsPropertyGeneratedEvent::class  => 'onGeneratedTsProperty',
        ];
    }

    public function onGeneratedTsInterface(TsInterfaceGeneratedEvent $event): void
    {
        $tsInterface = $event->tsInterface;
        $classNode   = $event->classNode;

        // ...
    }

    public function onGeneratedTsEnum(TsEnumGeneratedEvent $event): void
    {
        $tsEnum   = $event->tsEnum;
        $enumNode = $event->enumNode;

        // ...
    }

    public function onGeneratedTsProperty(TsPropertyGeneratedEvent $event): void
    {
        $tsProperty   = $event->tsProperty;
        $propertyNode = $event->propertyNode;

        // ...
    }
}

   ⬆   

💩 Dumper

If you do want to implement your own way of initiating the dump process you can inject the dumper service into your own services.
This of course can be done via all the various different ways of dependency injection and not only the one shown here:

<?php

declare(strict_types=1);

namespace App\Service;

use Brainshaker95\PhpToTsBundle\Model\Config\FileType;
use Brainshaker95\PhpToTsBundle\Model\Config\PartialConfig;
use Brainshaker95\PhpToTsBundle\Service\Dumper;

final class MyService
{
    public function __construct(
        private readonly Dumper $dumper,
    ) {}
    
    public function doTheThingsAndStuff(): void
    { 
        // See method descriptions for more detail
        $this->dumper->dumpDir();
        $this->dumper->dumpFiles(['path/to/file1', 'path/to/file2']);
        $this->dumper->dumpFile('path/to/file', new PartialConfig(fileType: FileType::TYPE_DECLARATION));
        $this->dumper->getTsInterfacesFromFile('path/to/file');
    }
}

   ⬆   

🙈 Known Limitations

  • All class identifiers used need to point to classes tagged with the AsTypeScriptable attribute, otherwise invalid TypeScript interfaces will be generated.
  • Types are only recognized as class identifiers if they start with an uppercase letter.
  • Multiline @deprecated and @template descriptions cannot contain empty lines between paragraphs. Only a single new line can be used as a separator. All other lines will be considered as part of the property description.
  • No support for nested readonly types for array shapes. Only the array property itself will be marked as readonly, which would technically allow nested properties to be modified.
  • No support for array shapes where some items have keys and some do not.
  • No support for value-of on backed enums.
  • No support for automatically removing generated TypeScript files when corresponding TypeScriptable is deleted. (See: #27)

   ⬆   

🔨 TODOs / Roadmap

  • Document example TypeScriptable class
  • Document example TypeScriptable enum
  • Document usage of Hidden attribute
  • Document usage of TsController

   ⬆   

❤️ Contributing

Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are greatly appreciated.

If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". Don't forget to give the project a star! Thanks again!

  1. Fork the Project
  2. Create your Feature Branch => git checkout -b feature/my-new-feature
  3. Commit your Changes => git commit -m 'feat(my-new-feature): add some awesome new feature'
  4. Push to the Branch => git push origin feature/my-new-feature
  5. Open a Pull Request

   ⬆   

⭐ License

Distributed under the MIT License. See LICENSE for more information.

   ⬆   

🌐 Acknowledgments

   ⬆