├── .gitignore ├── Command └── GenerateCommand.php ├── Entity └── Person.php ├── LICENSE ├── Parser ├── ParserInterface.php ├── Property.php └── Visitor.php ├── README.md ├── TypeScriptGeneratorBundle.php └── composer.json /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | composer.lock 4 | -------------------------------------------------------------------------------- /Command/GenerateCommand.php: -------------------------------------------------------------------------------- 1 | setName('typescript:generate-interfaces') 22 | ->setDescription('Generate TypeScript interfaces from PHP classes in a directory') 23 | ->addArgument( 24 | 'fromDir', 25 | InputArgument::REQUIRED, 26 | 'The directory to scan for suitable classes' 27 | ) 28 | ->addArgument( 29 | 'toDir', 30 | InputArgument::OPTIONAL, 31 | 'Where to export generated classes' 32 | ) 33 | ; 34 | } 35 | 36 | protected function execute(InputInterface $input, OutputInterface $output) 37 | { 38 | 39 | $projectDir = $this->getContainer()->get('kernel')->getProjectDir(); 40 | $fromDir = $input->getArgument('fromDir'); 41 | $toDir = $input->getArgument('toDir'); 42 | 43 | if(!$toDir){ 44 | $toDir = 'typescript'; 45 | 46 | } 47 | 48 | $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); 49 | $traverser = new NodeTraverser(); 50 | 51 | $fs = new Filesystem(); 52 | $finder = new Finder(); 53 | $finder->files('*.php')->in( $projectDir . '/' . $fromDir); 54 | 55 | foreach ($finder as $file) { 56 | 57 | $visitor = new Visitor(); 58 | $traverser->addVisitor($visitor); 59 | $code = $file->getContents(); 60 | 61 | try { 62 | 63 | $stmts = $parser->parse($code); 64 | $stmts = $traverser->traverse($stmts); 65 | 66 | if($visitor->getOutput()){ 67 | $targetFile = $toDir . '/' . str_replace( '.php','.d.ts', $file->getFilename()); 68 | $fs->dumpFile($targetFile,$visitor->getOutput()); 69 | $output->writeln('created interface ' . $targetFile); 70 | } 71 | 72 | } catch (\ParseError $e) { 73 | $output->writeln('Parse error: ' .$e->getMessage()); 74 | } 75 | 76 | } 77 | 78 | } 79 | } -------------------------------------------------------------------------------- /Entity/Person.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | } 16 | 17 | public function __toString() 18 | { 19 | $result = "interface {$this->name} {\n"; 20 | $result .= implode(",\n", array_map(function ($p) { return " " . (string)$p;}, $this->properties)); 21 | $result .= "\n}"; 22 | $result .= "\n"; 23 | $result .= "declare var {$this->name}: {$this->name};\n"; 24 | return $result; 25 | } 26 | } -------------------------------------------------------------------------------- /Parser/Property.php: -------------------------------------------------------------------------------- 1 | name = $name; 15 | $this->type = $type; 16 | } 17 | 18 | public function __toString() 19 | { 20 | return "{$this->name}: {$this->type}"; 21 | } 22 | } -------------------------------------------------------------------------------- /Parser/Visitor.php: -------------------------------------------------------------------------------- 1 | getDocComment() && strpos($class->getDocComment()->getText(), "@TypeScriptMe") !== false) { 26 | $this->isActive = true; 27 | $this->output[] = $this->currentInterface = new ParserInterface($class->name); 28 | } 29 | } 30 | 31 | if ($this->isActive) { 32 | if ($node instanceof PhpParser\Node\Stmt\Property) { 33 | /** @var PhpParser\Node\Stmt\Property $property */ 34 | $property = $node; 35 | 36 | if ($property->isPublic()) { 37 | $type = $this->parsePhpDocForProperty($property->getDocComment()); 38 | $this->currentInterface->properties[] = new Property($property->props[0]->name, $type); 39 | } 40 | } 41 | } 42 | } 43 | 44 | public function leaveNode(Node $node) 45 | { 46 | if ($node instanceof PhpParser\Node\Stmt\Class_) { 47 | $this->isActive = false; 48 | } 49 | } 50 | 51 | /** 52 | * @param \PhpParser\Comment|null $phpDoc 53 | */ 54 | private function parsePhpDocForProperty($phpDoc) 55 | { 56 | $result = "any"; 57 | 58 | if ($phpDoc !== null) { 59 | if (preg_match('/@var[ \t]+([a-z0-9]+)/i', $phpDoc->getText(), $matches)) { 60 | $t = trim(strtolower($matches[1])); 61 | 62 | if ($t === "int") { 63 | $result = "number"; 64 | } 65 | elseif ($t === "string") { 66 | $result = "string"; 67 | } 68 | } 69 | } 70 | 71 | return $result; 72 | } 73 | 74 | public function getOutput() 75 | { 76 | return implode("\n\n", array_map(function ($i) { return (string)$i;}, $this->output)); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP Classes to TypeScript Interfaces Generator Bundle 2 | ====== 3 | 4 | A Symfony bundle that adds a command to extract [TypeScript interface](https://www.typescriptlang.org/docs/handbook/interfaces.html) from PHP classes. Based on [the example from Martin Vseticka](https://stackoverflow.com/questions/33176888/export-php-interface-to-typescript-interface-or-vice-versa?answertab=votes#tab-top) this bundle uses [the PHP-Parser library](https://github.com/nikic/PHP-Parser) and annotations. This is currently very basic feature wise, but it does work. 5 | 6 | TypeScript is a superscript of JavaScript that adds strong typing and other features on top of JS. Automatically generated classes can be useful, for example when using a simple JSON API to communicate to a JavaScript client. This way you can get typing for your API responses in an easy way. 7 | 8 | This is currently tightly coupled to the Symfony Framework, but could be extracted. Feel free to build on this or use as inspiration to build something completely different. 9 | 10 | ## Installation 11 | 12 | As a Symfony bundle you'll need to start by add the package to your project with composer: 13 | 14 | ``` 15 | $ composer req janit/typescript-generator-bundle 16 | ``` 17 | 18 | After this you'll need to activate the bundle in your `app/AppKernel.php` file: 19 | 20 | ``` 21 | new Janit\TypeScriptGeneratorBundle\TypeScriptGeneratorBundle() 22 | ``` 23 | 24 | Once this is done you should have the added command in place in your console and you can run: 25 | 26 | ``` 27 | $ php bin/console typescript:generate-interfaces 28 | ``` 29 | 30 | This will yield an error because the command expects `fromDir` as a parameter on where to scan for PHP classes. 31 | 32 | NOTE: These instructions are for Symfony Standard Edition (3.3), but the bundle should work with Symfony Flex as well. 33 | 34 | ## Usage 35 | 36 | The command scans directories recursively for all `.php` files. It will only generate Type Definitions (interfaces) for files with appropriate annotations. 37 | 38 | To generate interfaces, create a new class in `src/AppBundle/Entity/Person.php` and enter the following: 39 | 40 | ```php 41 |