2015-10-16 56 views
4

我正在嘗試使用Typescript,並且在我當前的合同中我使用PHP編寫後端代碼。將PHP接口導出到Typescript接口,反之亦然?

在幾個項目中,我編寫了用於AJAX響應的Typescript接口,我的後端代碼給出了前端開發人員(有時候也是我,有時是其他人)知道期望什麼並獲得類型檢查等上。

在編寫了一些這樣的後端服務之後,似乎接口和響應的相關類也應該存在於PHP端。這讓我覺得如果我可以用兩種語言中的一種編寫它們並運行一些構建時工具(我會在運行Typescript編譯器之前使用一個gulp任務調用它)來導出這些工具與其他語言的接口。

這樣的事情是否存在?可能嗎?實際的?

(我知道PHP不是強類型,但如果界面是用PHP寫的有可能是某種類型的暗示存在諸如出口識別並延續到打字稿文檔字符串)。

+1

與PHP7(和黑客),你擁有所有基本和複雜類型(對象,數組和可調用對象是在PHP 5 typehintable開始.x +)作爲您可以使用的類型提示(它們如何由運行時強制執行與導出無關)。所以你可以避免解析docblocks。無論哪種方式,您可以使用[php-parser](https://github.com/nikic/PHP-Parser)生成AST,並基於此生成打印文件。不應該複雜。 (我對typecript沒有更深入的瞭解,所以我不知道類型系統的匹配程度如何,但是由於它似乎受c#啓發,它們應該有點兼容) – Rangad

+0

這很好理解。所以只剩下這樣的事情是否已經存在。 – tremby

回答

3

你可以用驚人nikic/PHP-Parser創建一個工具,用於將選定的PHP類(phpDoc中的@TypeScriptMe字符串)轉換爲TypeScript接口。以下腳本非常簡單,但我認爲您可以擴展它,並且可以自動生成TypeScript接口並可能通過git跟蹤更改。

此輸入:

<?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" 
}