TokyoFacebookTwitterHatenaPocketプログラミング言語に自分がほしい機能を追加できたら...。マクロとプリプロセッサー「YAY!」を使って、PHPを拡張する方法に挑戦してみました。これからちょっとおもしろいことをしてみます。前にPythonのrangeの文法をPHPマクロに転用する実験をしました。そのあと、名の知れたSaraMGがRFCについて言及し、LordKabeloがC#スタイルのgetterとsetterを代わりに加えることを提案しました。外部から言語の新機能を提案し、実装することがいかに時間を要するか分かったうえで、私はエディターに向かいました。この記事のコードはGithubで入手できます。^5.6|^7.0で動作します。マクロの復習マクロについて書いてからしばらくたったので(初めての人もいるかもしれません)、次のコードで復習します。many[1..3];下のように有効なPHPにします。$lower);');※私が書いた記事でコードの動作を確認できます。重要な点は、パーサーがコードの文字列をトークンに分割してマクロのパターンを構築し、そのパターンを再帰的に新たなコードに適用する方法を理解することです。マクロライブラリー(YAY!)はドキュメントの整備が遅れていて、正確なパターンの書き方や最後に生成される有効なコードを調べるのは簡単ではありません。新しいアプリケーションごとに、この記事のような解説がないと、ほかの人は理解できません。ベースを作る手元にあるPHPのアプリケーションに、C#に似たgetterとsetterを加えます。取り掛かる前に、作業のベースとなるコードが必要です。この機能を追加で必要としているクラスに共通する特徴を持ったコードが良いでしょう。クラスの定義を点検し、検出した特別なプロパティとコメントに動的なgetterとsetterメソッドを作成するコードを実装します。最初は、特別なメソッド名のフォーマットと、マジックメソッド__getと__setを定義します。}}名前が__get_や__set_から始まるメソッドはこれから定義するプロパティに関連付けます。次のようなコードを考えてください。};}これを次のようなコードに変換します。今回の難所は必要なマクロを定義することです。ドキュメントが整備されていない(広く利用されていない)ので、例外を知らせてくれるエラーメッセージを頼りに試行錯誤することになります。次に示すのは数時間を費やして到達したパターンです。}}この2つのマクロの動作を説明します。}をマッチさせ、先ほど作成したAccessorTraitを挿入する。$class->barなどに接続するgetterとsetterのブロックをマッチさせ、通常のプロパティ定義で置換し、いくつかの個別のメソッドの定義を続ける。}ブロックの内容をそのまま関数内に入れ込むこのコードを実行すると、エラーが出ます。マクロプロセッサーは··unvar関数を標準ではサポートしていないためです。次は$typeをtypeに変換するために追加したものです。Yayの中身を分からなくても、コードの動作を理解できます。TokenStreamをstringに(この文脈で)キャストすると、tokenの参照先のstring値を得られます。この場合は··unvar(T_VARIABLE·var)で、string操作を実行します。$tsは"T_VARIABLE·var"ではなく"$type"になります。通常このマクロは、適用対象のスクリプトの内部に記載して実行します。言い換えると、次のようなスクリプトが書けます。};}そして次のようなコマンドを使って実行できます。src/Sprocket.php最後に、このコードを次のように(ComposerのPSR-4オートロードとあわせて)使います。Sprocket変換の自動化これを手作業でやるのは得策ではありません。src/Sprocket.preを変更するたびにbashコマンドを実行したいなんて誰も思いません。幸いにも自動化できます!最初にカスタムオートローダーを定義します。true);このファイルをautoload.phpとして保存し、ドキュメントに書かれている通り、filesを使ってComposeのオートローダーに含めてオートロードできます。specificationの実装例から転用しています。ComposerのPSR-4定義ファイルを取得し、文頭ごとにロードしているクラスに一致するかチェックしています。一致すれば、可能性があるパスをチェックして、カスタムコードを定義しているfile.preを見つけます。macros.preファイル(プロジェクトのベースディレクトリにある)の中身を取得し、macros.preと一致したファイルの中身から中間ファイルを作成します。Yayに渡すファイルからマクロを使えます。Yayがfile.pre.interimをfile.phpにコンパイルしたあとに、file.pre.interimを削除します。このプロセスのコードは次のように書けます。$php;spl_autoload_registerの末尾にある2つのboolean引数に着目してください。1つ目はこのオートローダーがローディングのエラーに対して例外を投げるかどうかです。2つ目はこのオートローダーをスタックに積むかどうかで、これにより、Composerのオートローダーより前になり、Composerがfile.phpをロードしようとする前にfile.preを変換できます!プラグインのフレームワークを作成この自動化はすばらしいものの、プロジェクトごとに繰り返すのは非効率です。requireする方法を考えます。最初は次のファイルが含まれるレポジトリを作ります。以下のファイルをオートロードするfunctions.php:マクロパス関数を作る(ほかのライブラリーが独自のファイルを動的に追加できるようになる)expanders.php:··unvarのようなexpander関数を作るautoload.php:Composerのオートローダーを補強し、それぞれのライブラリーのマクロファイルをコンパイル後の.preファイルにロードする以下はcomposer.jsonです。true}次はfunctions.phpです。$GLOBALS[GLOBAL_KEY];}マクロファイルのパスを$GLOBALSに保存することに二の足を踏むかもしれませんが、パスを保存する方法はほかにいくらでもあり、ここでは重要なポイントではありません。パターンを使ってみるために、もっとも簡単な方法を採用したまでです。下はexpanders.phpです。;}次はautoload.phpです。true);以上で追加のマクロプラグインはこの関数を使って独自のコードをシステムに組み込めます。言語の新機能を作るプラグインのコードができたので、クラスのアクセッサーを、自動的に適用される独立した機能へ作り変えます。そのために追加のファイルがいくつか必要です。composer.json:ベースプラグインレポジトリーを要求し、次のファイルをオートロードするmacros.pre:このプラグインのマクロコードfunctions.php:アクセッサーマクロをベースプラグインシステムに組み入れるsrc/AccessorsTrait.php:前出からほぼ変更なし以下はcomposer.jsonです。true}次はfunctions.phpです。"/macros.pre");下はmacros.preです。...}このマクロファイルは先ほどより少し冗長になりました。見つけられていませんが、アクセッサーが定義される環境をもっとスマートに扱う方法があるとは思います。統合するすべてをきれいにパッケージ化できたので、言語の新機能はシンプルに使えます。次の短いデモを見てください!このプラグインのレポジトリーはGithubで入手できます。plugin(クラスアクセッサーのプラグイン)最後にどのプラグインについても言えることですが、誤った使い方もできてしまいます。マクロも例外ではありません。このコードはおもしろい実験ではあるものの、まだ本番環境には適しません。コードの使い勝手が悪いとコメントしたくなるかもしれませんが、そこはご容赦ください。この記事の形のままコードを使うことをおすすめはしません。言いつつも、これはおもしろいアイデアだと思ってもらえたことでしょう。クラスアクセッサーレポジトリを出発点にできます。アイデアが有効か判断できるよう、プラグインレポジトリを使って自動化することもできるでしょう。この記事を執筆して以来、必死になって基礎ライブラリーに取り組み、このコードを組み込んでデモをするWebサイトhttps://preprocess.ioを立ち上げました。アルファ版ですが、この記事で取り上げたすべてのコードとそれ以外にもいくつかのコードを紹介しています。マクロを試してみたい人のために、手軽なREPLも用意しました。Pittライター兼プログラマーでSilverStripeに勤務しています。普段はアプリケーションアーキテクチャーに取り組んでいますが、ときどきコンパイラーやロボットを作ることもあります。