Skip to content
Advertisement

Export PHP interface to Typescript interface, or vice versa?

I’m experimenting with Typescript, and at my current contract I code backend in PHP.

In a couple of projects I’ve written Typescript interfaces for the sort of AJAX responses my back end code gives so that the frontend developer (sometimes also me, sometimes someone else) knows what to expect and gets type checking and so on.

After writing a few such back end services it seems like the interface and related classes for the responses should exist on the PHP side too. And that makes me think that it’d be nice if I could write them in just one of the two languages and run some build-time tool (I’d invoke it with a gulp task, before the Typescript compiler runs) to export these interfaces to the other language.

Does such a thing exist? Is it possible? Practical?

(I realize PHP is not strongly typed, but if the interfaces were written in PHP there could be some type hinting there such as docstrings which the exporter recognizes and carries over to Typescript.)

Advertisement

Answer

You can use amazing nikic/PHP-Parser to create a tool for converting selected PHP classes (those with @TypeScriptMe string in phpDoc) to TypeScript interfaces quite easily. The following script is really simple one but I think you can expand it and you can generate TypeScript interfaces automatically and possibly track changes through git.

Example

For this input:

<?php
/**
 * @TypeScriptMe
 */
class Person
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var int
     */
    public $age;

    /**
     * @var stdClass
     */
    public $mixed;

    /**
     * @var string
     */
    private $propertyIsPrivateItWontShow;
}

class IgnoreMe {

    public function test() {

    }
}

you’ll get:

interface Person {
  name: string,
  age: number,
  mixed: any
}

Source codes

index.php:

<?php

namespace TypeScript {

    class Property_
    {
        /** @var string */
        public $name;
        /** @var string */
        public $type;

        public function __construct($name, $type = "any")
        {
            $this->name = $name;
            $this->type = $type;
        }

        public function __toString()
        {
            return "{$this->name}: {$this->type}";
        }
    }

    class Interface_
    {
        /** @var string */
        public $name;
        /** @var Property_[] */
        public $properties = [];

        public function __construct($name)
        {
            $this->name = $name;
        }

        public function __toString()
        {
            $result = "interface {$this->name} {n";
            $result .= implode(",n", array_map(function ($p) { return "  " . (string)$p;}, $this->properties));
            $result .= "n}";
            return $result;
        }
    }
}

namespace MyParser {

    ini_set('display_errors', 1);
    require __DIR__ . "/vendor/autoload.php";

    use PhpParser;
    use PhpParserNode;
    use TypeScript;

    class Visitor extends PhpParserNodeVisitorAbstract
    {
        private $isActive = false;

        /** @var TypeScript/Interface_[] */
        private $output = [];

        /** @var TypeScriptInterface_ */
        private $currentInterface;

        public function enterNode(Node $node)
        {
            if ($node instanceof PhpParserNodeStmtClass_) {

                /** @var PhpParserNodeStmtClass_ $class */
                $class = $node;
                // If there is "@TypeScriptMe" in the class phpDoc, then ...
                if ($class->getDocComment() && strpos($class->getDocComment()->getText(), "@TypeScriptMe") !== false) {
                    $this->isActive = true;
                    $this->output[] = $this->currentInterface = new TypeScriptInterface_($class->name);
                }
            }

            if ($this->isActive) {
                if ($node instanceof PhpParserNodeStmtProperty) {
                    /** @var PhpParserNodeStmtProperty $property */
                    $property = $node;

                    if ($property->isPublic()) {
                        $type = $this->parsePhpDocForProperty($property->getDocComment());
                        $this->currentInterface->properties[] = new TypeScriptProperty_($property->props[0]->name, $type);
                    }
                }
            }
        }

        public function leaveNode(Node $node)
        {
            if ($node instanceof PhpParserNodeStmtClass_) {
                $this->isActive = false;
            }
        }

        /**
         * @param PhpParserComment|null $phpDoc
         */
        private function parsePhpDocForProperty($phpDoc)
        {
            $result = "any";

            if ($phpDoc !== null) {
                if (preg_match('/@var[ t]+([a-z0-9]+)/i', $phpDoc->getText(), $matches)) {
                    $t = trim(strtolower($matches[1]));

                    if ($t === "int") {
                        $result = "number";
                    }
                    elseif ($t === "string") {
                        $result = "string";
                    }
                }
            }

            return $result;
        }

        public function getOutput()
        {
            return implode("nn", array_map(function ($i) { return (string)$i;}, $this->output));
        }
    }

    ### Start of the main part


    $parser = new PhpParserParser(new PhpParserLexerEmulative);
    $traverser = new PhpParserNodeTraverser;
    $visitor = new Visitor;
    $traverser->addVisitor($visitor);

    try {
        // @todo Get files from a folder recursively
        //$code = file_get_contents($fileName);

        $code = <<<'EOD'
<?php
/**
 * @TypeScriptMe
 */
class Person
{
    /**
     * @var string
     */
    public $name;

    /**
     * @var int
     */
    public $age;

    /**
     * @var stdClass
     */
    public $mixed;

    /**
     * @var string
     */
    private $propertyIsPrivateItWontShow;
}

class IgnoreMe {

    public function test() {

    }
}

EOD;

        // parse
        $stmts = $parser->parse($code);

        // traverse
        $stmts = $traverser->traverse($stmts);

        echo "<pre><code>" . $visitor->getOutput() . "</code></pre>";

    } catch (PhpParserError $e) {
        echo 'Parse Error: ', $e->getMessage();
    }
}

composer.json

{
    "name": "experiment/experiment",
    "description": "...",
    "homepage": "http://example.com",
    "type": "project",
    "license": ["Unlicense"],
    "authors": [
        {
            "name": "MrX",
            "homepage": "http://example.com"
        }
    ],
    "require": {
        "php": ">= 5.4.0",
        "nikic/php-parser": "^1.4"
    },
    "minimum-stability": "stable"
}
User contributions licensed under: CC BY-SA
3 People found this is helpful
Advertisement