🐶

さわって慣れるPHP Parser

2023/06/25に公開

先日PHPカンファレンス福岡2023で登壇する機会をいただきました。

Rectorと目指す 負債をためないシステム開発~はじめの一歩~

以下の様なフィードバックをいただいた(ありがとうございます!)ので補足説明したいと思います。

  • 発表内容は概ね理解できた気がするが手を動かせる自信がない。
  • Identifierノードがわかりにくい。

前者については、まずメンタルモデル(発表参照)を作り、ノードを理解することから始めるのが有効と考えていて、ノード[1]を理解するにはPHP Parserを動かして結果を観察するのが有効なのではないかと考えています。というわけで、Identifierノードを観察するというケーススタディで具体的な手順を書いてみようと思います。

まずIdentifierの言葉から確認すると、発音はaidéntifàiər(アイデンティファイア的な)で、意味は識別子です。

ではPHP Parserをインストールします。

$ composer require --dev nikic/php-parser

解析対象のコード(TheWorld.php)をしたためます。(PHPとしての適切さは度外視し、ノードを観察するためだけのコードです。)

<?php
class TheWorld {}
function theWorld() {}

解析します。

$ vendor/bin/php-parse TheWorld.php 
====> File TheWorld.php:
==> Node dump:
array(
    0: Stmt_Class(
        attrGroups: array(
        )
        flags: 0
        name: Identifier(
            name: TheWorld
        )
        extends: null
        implements: array(
        )
        stmts: array(
        )
    )
    1: Stmt_Function(
        attrGroups: array(
        )
        byRef: false
        name: Identifier(
            name: theWorld
        )
        params: array(
        )
        returnType: null
        stmts: array(
        )
    )
)

色々でてますがサラリと流してIdentifierにのみに注意を払います。
Stmt_ClassStmt_Functionの中にIdentifierがいることがわかります。

前者にはクラス名が入っていて、後者には関数名が入っています。

        Identifier(
            name: TheWorld
        )

        Identifier(
            name: theWorld
        )

ひとまず何者なのか目視確認できましたが、仕様の裏どりもしたいと思います。

上記の解析結果では何のオブジェクトなのかがわかりにくいので--var-dumpモードでも解析してみます。

$ vendor/bin/php-parse --var-dump TheWorld.php 
====> File TheWorld.php:
==> var_dump():
array(2) {
  [0]=>
  object(PhpParser\Node\Stmt\Class_)#1202 (8) {
    ["flags"]=>
    int(0)
    ["extends"]=>
    NULL
    ["implements"]=>
    array(0) {
    }
    ["name"]=>
    object(PhpParser\Node\Identifier)#1201 (2) {
      ["name"]=>
      string(8) "TheWorld"
      ["attributes":protected]=>
      array(4) {
        ["startLine"]=>
        int(2)
        ["startFilePos"]=>
        int(12)
        ["endLine"]=>
        int(2)
        ["endFilePos"]=>
        int(19)
      }
    }
    ["stmts"]=>
    array(0) {
    }
    ["attrGroups"]=>
    array(0) {
    }
    ["namespacedName"]=>
    NULL
    ["attributes":protected]=>
    array(4) {
      ["startLine"]=>
      int(2)
      ["startFilePos"]=>
      int(6)
      ["endLine"]=>
      int(2)
      ["endFilePos"]=>
      int(22)
    }
  }
  [1]=>
  object(PhpParser\Node\Stmt\Function_)#1204 (8) {
    ["byRef"]=>
    bool(false)
    ["name"]=>
    object(PhpParser\Node\Identifier)#1203 (2) {
      ["name"]=>
      string(8) "theWorld"
      ["attributes":protected]=>
      array(4) {
        ["startLine"]=>
        int(3)
        ["startFilePos"]=>
        int(33)
        ["endLine"]=>
        int(3)
        ["endFilePos"]=>
        int(40)
      }
    }
    ["params"]=>
    array(0) {
    }
    ["returnType"]=>
    NULL
    ["stmts"]=>
    array(0) {
    }
    ["attrGroups"]=>
    array(0) {
    }
    ["namespacedName"]=>
    NULL
    ["attributes":protected]=>
    array(4) {
      ["startLine"]=>
      int(3)
      ["startFilePos"]=>
      int(24)
      ["endLine"]=>
      int(3)
      ["endFilePos"]=>
      int(45)
    }
  }
}

情報量が増えてわけわかりにくくなりましたが、バシバシ無視しましょう。以下に注目します。

PhpParser\Node\Stmt\Class_
PhpParser\Node\Stmt\Function_
PhpParser\Node\Identifier

IDEでジャンプするか、PHPStanのドキュメントを参照するなどしてこれらの定義を見に行きます。

https://apiref.phpstan.org/1.9.x/PhpParser.Node.Stmt.Class_.html

ClassLikeを継承しているのでそちらも確認しに行きます。
https://apiref.phpstan.org/1.9.x/PhpParser.Node.Stmt.ClassLike.html

Identifier|null型のnameというプロパティを持っていることが分かります。

Identifierも確認します。
https://apiref.phpstan.org/1.9.x/PhpParser.Node.Identifier.html

Represents a non-namespaced name.

名前空間をつけない名前を表すものであるということがわかりました。
Function_も同じ要領で確認します。

こんな感じで簡単なコードを書いて解析して観察することでノードの理解をこつこつ深めております。そうしてまとめた資料がこちらです。

Rectorはコードをリファクタします。
説明に用いた事例は呼び出すメソッド名をリファクタするというものでした。

PhpParser\Node\Expr\MethodCallもメソッド名を表すのにIdentifierをもっており、Identifierを取り換えることでメソッド名を書き換えることが可能という話でした。

脚注
  1. PHP Parserで学ぶPHP(P26~40) ↩︎

Discussion