Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

GoからPHPを呼び出す方法

サーバーサイドエンジニアの田実です!

ヤプリではGoとPHPを使っています。ということで(?)、GoからPHPを呼び出す方法について紹介します。 なおこの記事はPHPからGoを呼び出す方法の姉妹投稿(?)となっております。

※この記事は Go Advent Calendar 2021の11日目の記事です!

Embed SAPIを使ってPHPを呼び出す

GoからPHPを直接呼ぶ方法として deuill/go-phpというパッケージがあります。 github.com

Embed SAPIというC言語からPHPを呼び出せるAPIを使って、Go(cgo)からPHPを呼び出しています。

package main

import (
    "os"

    php "github.com/deuill/go-php"
)

func main() {
    engine, _ := php.New()

    context, _ := engine.NewContext()
    context.Output = os.Stdout

    context.Exec("index.php")
    engine.Destroy()
}

Dockerで環境構築する場合は以下のようにして実行します。本家のGoのバージョンが古かったりするのでforkして変更したものを使ってます。

$ git clone git@github.com:tzmfreedom/go-php.git
$ cd go-php
$ git checkout -b update_golang_version origin/update_golang_version
$ make docker-image PHP_VERSION=7.2.34
$ docker run --rm -w /app -v $(pwd)/example:/app -it deuill/go-php:7.2.34 "go run main.go"

一番筋が良さそうな方法ではあるのですが、手元で動作したところ正常動作するのはPHP7.2系まででした…w
PHPのインストールが必要なので、結局のところ php {ファイル名} を exec.Command を使って実行するのと原理的には同じような気もします。

PHPをパースしてGoに変換

z7zmey/php-parser というGoで書かれたPHPのパーサーを使ってASTを構築し、Goに変換します。

変換用のCLIツールはこちらに実装しました。 github.com

以下でインストールできます。

$ go install github.com/tzmfreedom/go-generator@latest

標準入力にPHPのコードを入力すると

$ echo '<?php function TestFunction(string $word) { echo $word; }' | go-generator

標準出力にGoのコードが出力されます。

package hoge

import "fmt"

func TestFunction(word string) {
fmt.Println(word)
}

あとは変換処理をMakefileに組み込んで事前に変換するようにすればGoからPHPを呼び出しているような雰囲気になります。 ただ、全構文、クラス、関数に対応するのは現実的に厳しい感じですね…w

PHPをパースして実行

z7zmey/php-parser を使ってASTを構築し、自前のインタープリターで実行します。 github.com

使い方はこんな感じです。

package main

import (
    "os"

    interpreter "github.com/tzmfreedom/php-interpreter"
)

func main() {
    src := []byte(`<? echo "Hello world";`)
    err := interpreter.Run(src, "7.4", false)
    if err != nil {
        panic(err)
    }
}

内部実装的にはTree Walk InterpreterなコードでPHPを実行しています。

func Parse(src []byte, version string) error {
    parser := php7.NewParser(src, version)
    parser.Parse()

    for _, e := range parser.GetErrors() {
        return errors.New(e.String())
    }

    rootNode := parser.GetRootNode()
    rootNode.Walk(&Interpreter{})
    return nil
}

func (d *Interpreter) EnterNode(w walker.Walkable) bool {
    switch n := w.(type) {
    case *node.Root:
    case *stmt.Echo:
        n.Exprs[0].Walk(d)
        value := d.pop()
        switch v := value.(type) {
        case string:
            fmt.Println(v[1 : len(v)-1])
        default:
            fmt.Println(v)
        }
        return false
    case *scalar.String:
        d.push(n.Value)
// ...
    }

    return true
}

事前処理がなく直接PHPを実行でき、PHPのインストールも不要なので便利そうですが、変換パターンと同様で本気で全パターンを実装するのは厳しいです。

まとめ

GoからPHPを呼び出す方法を紹介しました!

Embed SAPIを使って呼び出す方法は様々なコードを実行できる反面、PHPのインストールに若干手間がかかります。PHPからGoに移行したいようなケース*1ではコード変換のアプローチが役立ちそうです。PHPをパースして実行するパターンはDSLとして使えるミニPHPの実行基盤として考えると面白いかもしれません。

*1:ヤプリではPHPアプリケーションをGoに移行するプロジェクトがあったりする