Export PHP interface to Typescript interface or vice versa?

I am experimenting with Typescript, and in my current contract I am encoding a server in PHP.

In several projects, I wrote Typescript interfaces to sort the AJAX responses that my code ends, so the interface designer (sometimes also me, sometimes someone else) knows what to expect and gets type checking and so on.

After writing some of these back end services, it seems that the interface and related classes for the answers should exist on the PHP side as well. And that makes me think that it would be nice if I could only write them in one of the two languages ​​and run some build-time tool (I would call it with the gulp task before the Typescript compiler starts) to export these interfaces to another language.

Is there such a thing? Is it possible? Practical?

(I understand that PHP is not strongly typed, but if the interfaces were written in PHP, there might be some type of hint, for example, docstrings, which the exporter recognizes and transfers to Typescript.)

+4
source share
1 answer

You can use the awesome nikic / PHP-Parser to create a tool for converting selected PHP classes (those with @TypeScriptMestring to phpDoc) to TypeScript quite easily. The following script is really simple, but I think you can extend it, and you can automatically generate TypeScript interfaces and maybe 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() {

    }
}

:

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

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 PhpParser\Node;
    use TypeScript;

    class Visitor extends PhpParser\NodeVisitorAbstract
    {
        private $isActive = false;

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

        /** @var TypeScript\Interface_ */
        private $currentInterface;

        public function enterNode(Node $node)
        {
            if ($node instanceof PhpParser\Node\Stmt\Class_) {

                /** @var PhpParser\Node\Stmt\Class_ $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 TypeScript\Interface_($class->name);
                }
            }

            if ($this->isActive) {
                if ($node instanceof PhpParser\Node\Stmt\Property) {
                    /** @var PhpParser\Node\Stmt\Property $property */
                    $property = $node;

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

        public function leaveNode(Node $node)
        {
            if ($node instanceof PhpParser\Node\Stmt\Class_) {
                $this->isActive = false;
            }
        }

        /**
         * @param \PhpParser\Comment|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("\n\n", array_map(function ($i) { return (string)$i;}, $this->output));
        }
    }

    ### Start of the main part


    $parser = new PhpParser\Parser(new PhpParser\Lexer\Emulative);
    $traverser = new PhpParser\NodeTraverser;
    $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 (PhpParser\Error $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"
}
+3

Source: https://habr.com/ru/post/1611984/


All Articles