サーバーサイドエンジニアの田実です!
ヤプリでは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に移行するプロジェクトがあったりする