Nette PhpGenerator

クラス、関数、あるいはファイルまるごとの PHP のコードを生成する道具をお探しですか。
  • PHP の最新の機能(プロパティフック、enum、アトリビュートなど)にすべて対応しています
  • 既存のクラスを簡単に書き換えられます
  • 出力は PSR-12 / PER のコーディングスタイルに沿います
  • 成熟していて安定し、広く使われているライブラリ

インストール

ライブラリは Composerの道具でダウンロードしてインストールします。

composer require nette/php-generator

PHP との互換については互換の表をご覧ください。

クラス

ClassTypeでクラスを作る例から始めましょう。

$class = new Nette\PhpGenerator\ClassType('Demo');

$class
	->setFinal()
	->setExtends(ParentClass::class)
	->addImplement(Countable::class)
	->addComment("Class description.\nSecond line\n")
	->addComment('@property-read Nette\Forms\Form $form');

// 文字列にキャストするか echo するだけでコードを生成します:
echo $class;

これは次の結果を返します。

/**
 * Class description.
 * Second line
 *
 * @property-read Nette\Forms\Form $form
 */
final class Demo extends ParentClass implements Countable
{
}

コードの生成には printer も使えます。echo $class と違って、これはさらに設定できます

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class);

定数(Constantクラス)とプロパティ(Propertyクラス)を足せます。

$class->addConstant('ID', 123)
	->setProtected() // 定数の可視性
	->setType('int')
	->setFinal();

$class->addProperty('items', [1, 2, 3])
	->setPrivate() // または setVisibility('private')
	->setStatic()
	->addComment('@var int[]');

$class->addProperty('list')
	->setType('?array')
	->setInitialized(); // '= null' を出力します

これは次を生成します。

final protected const int ID = 123;

/** @var int[] */
private static $items = [1, 2, 3];
public ?array $list = null;

そしてメソッドも足せます。

$method = $class->addMethod('count')
	->addComment('Count it.')
	->setFinal()
	->setProtected()
	->setReturnType('?int') // メソッドの戻り値の型
	->setBody('return count($items ?: $this->items);');

$method->addParameter('items', []) // $items = []
	->setReference()           // &$items = []
	->setType('array');        // array &$items = []

結果はこうなります。

/**
 * Count it.
 */
final protected function count(array &$items = []): ?int
{
	return count($items ?: $this->items);
}

PHP 8.0 で入った promoted のパラメータはコンストラクタへ渡せます。

$method = $class->addMethod('__construct');
$method->addPromotedParameter('name');
$method->addPromotedParameter('args', [])
	->setPrivate();

結果はこうなります。

public function __construct(
	public $name,
	private $args = [],
) {
}

読み取り専用のプロパティとクラスは setReadOnly() の関数で印を付けられます。


足そうとしたプロパティ、定数、メソッド、トレイトがすでにあると、例外が投げられます。逆にパラメータは上書きされます。

クラスの要素は removeProperty()removeConstant()removeMethod()removeParameter() で取り除けます。

既存の MethodPropertyConstant のオブジェクトをクラスに足すこともできます。

$method = new Nette\PhpGenerator\Method('getHandle');
$property = new Nette\PhpGenerator\Property('handle');
$const = new Nette\PhpGenerator\Constant('ROLE');

$class = (new Nette\PhpGenerator\ClassType('Demo'))
	->addMember($method)
	->addMember($property)
	->addMember($const);

既存のメソッド、プロパティ、定数を cloneWithName() で違う名前に複製することもできます。

$methodCount = $class->getMethod('count');
$methodRecount = $methodCount->cloneWithName('recount');
$class->addMember($methodRecount);

インターフェースとトレイト

インターフェースとトレイトを作れます(InterfaceTypeTraitTypeのクラス)。

$interface = new Nette\PhpGenerator\InterfaceType('MyInterface');
$trait = new Nette\PhpGenerator\TraitType('MyTrait');

トレイトを使います。

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addTrait('SmartObject');
$class->addTrait('MyTrait')
	->addResolution('sayHello as protected')
	->addComment('@use MyTrait<Foo>');
echo $class;

結果はこうなります。

class Demo
{
	use SmartObject;
	/** @use MyTrait<Foo> */
	use MyTrait {
		sayHello as protected;
	}
}

enum

PHP 8.1 で入った enum は次のように簡単に作れます(EnumTypeクラス)。

$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs');
$enum->addCase('Diamonds');
$enum->addCase('Hearts');
$enum->addCase('Spades');

echo $enum;

結果はこうなります。

enum Suit
{
	case Clubs;
	case Diamonds;
	case Hearts;
	case Spades;
}

スカラーの対応する値を定めて、backed enum を作ることもできます。

$enum = new Nette\PhpGenerator\EnumType('Suit');
$enum->addCase('Clubs', '♣');
$enum->addCase('Diamonds', '♦');

case ごとに、addComment()addAttribute() でコメントやアトリビュートを足せます。

無名クラス

名前として null を渡せば、無名クラスになります。

$class = new Nette\PhpGenerator\ClassType(null);
$class->addMethod('__construct')
	->addParameter('foo');

echo '$obj = new class ($val) ' . $class . ';';

結果はこうなります。

$obj = new class ($val) {
	public function __construct($foo)
	{
	}
};

大域の関数

大域の関数のコードは GlobalFunctionクラスが生成します。

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('return $a + $b;');
$function->addParameter('a');
$function->addParameter('b');
echo $function;

// あるいは PSR-2 / PSR-12 / PER に沿った出力には PsrPrinter を使います
// echo (new Nette\PhpGenerator\PsrPrinter)->printFunction($function);

結果はこうなります。

function foo($a, $b)
{
	return $a + $b;
}

無名関数

無名関数(クロージャ)のコードは Closureクラスが生成します。

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('return $a + $b;');
$closure->addParameter('a');
$closure->addParameter('b');
$closure->addUse('c')
	->setReference();
echo $closure;

// あるいは PSR-2 / PSR-12 / PER に沿った出力には PsrPrinter を使います
// echo (new Nette\PhpGenerator\PsrPrinter)->printClosure($closure);

結果はこうなります。

function ($a, $b) use (&$c) {
	return $a + $b;
}

短いアロー関数

printer を使えば短いアロー関数も出力できます。

$closure = new Nette\PhpGenerator\Closure;
$closure->setBody('$a + $b');
$closure->addParameter('a');
$closure->addParameter('b');

echo (new Nette\PhpGenerator\Printer)->printArrowFunction($closure);

結果はこうなります。

fn($a, $b) => $a + $b;

メソッドと関数の見出し

メソッドは Methodクラスが表します。可視性、戻り値の型、コメント、アトリビュートなどを設定できます。

$method = $class->addMethod('count')
	->addComment('Count it.')
	->setFinal()
	->setProtected()
	->setReturnType('?int');

個々のパラメータは Parameterクラスが表します。ここでも思いつく限りの性質を設定できます。

$method->addParameter('items', []) // $items = []
	->setReference()           // &$items = []
	->setType('array');        // array &$items = []

// function count(array &$items = [])

可変長のパラメータ(splat 演算子とも呼ばれます)を定めるには setVariadic() を使います。

$method = $class->addMethod('count');
$method->setVariadic(true);
$method->addParameter('items');

これは次を生成します。

function count(...$items)
{
}

メソッドと関数の本体

本体は setBody() メソッドで一度に渡すことも、addBody() を繰り返し呼んで少しずつ(行ごとに)渡すこともできます。

$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('$a = rand(10, 20);');
$function->addBody('return $a;');
echo $function;

結果はこうなります。

function foo()
{
	$a = rand(10, 20);
	return $a;
}

変数を簡単に差し込む特別なプレースホルダを使えます。

単純なプレースホルダ ? です。

$str = 'any string';
$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addBody('return substr(?, ?);', [$str, $num]);
echo $function;

結果はこうなります。

function foo()
{
	return substr('any string', 3);
}

可変長のためのプレースホルダ ...? です。

$items = [1, 2, 3];
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->setBody('myfunc(...?);', [$items]);
echo $function;

結果はこうなります。

function foo()
{
	myfunc(1, 2, 3);
}

PHP 8 の名前付きのパラメータには ...?: を使えます。

$items = ['foo' => 1, 'bar' => true];
$function->setBody('myfunc(...?:);', [$items]);

// myfunc(foo: 1, bar: true);

プレースホルダはバックスラッシュ \? でエスケープします。

$num = 3;
$function = new Nette\PhpGenerator\GlobalFunction('foo');
$function->addParameter('a');
$function->addBody('return $a \? 10 : ?;', [$num]);
echo $function;

結果はこうなります。

function foo($a)
{
	return $a ? 10 : 3;
}

Printer と PSR への準拠

PHP のコードの生成には Printerクラスを使います。

$class = new Nette\PhpGenerator\ClassType('Demo');
// ...

$printer = new Nette\PhpGenerator\Printer;
echo $printer->printClass($class); // echo $class と同じ

ほかのすべての要素のコードも生成でき、printFunction()printNamespace() などのメソッドがあります。

出力が PSR-2 / PSR-12 / PER のコーディングスタイルに沿う PsrPrinterクラスもあります。

$printer = new Nette\PhpGenerator\PsrPrinter;
echo $printer->printClass($class);

振る舞いを変えたいですか。Printer クラスを継承して自分の版を作れます。次の変数を設定し直せます。

class MyPrinter extends Nette\PhpGenerator\Printer
{
	// この長さを超えると行を折り返します
	public int $wrapLength = 120;
	// 字下げの文字。空白の並びに替えられます
	public string $indentation = "\t";
	// プロパティのあいだの空行の数
	public int $linesBetweenProperties = 0;
	// メソッドのあいだの空行の数
	public int $linesBetweenMethods = 2;
	// クラス、関数、定数の 'use 文' のまとまりのあいだの空行の数
	public int $linesBetweenUseTypes = 0;
	// 関数とメソッドの開く波かっこの位置
	public bool $bracesOnNextLine = true;
	// パラメータがひとつなら、アトリビュートが付いていても promoted でも 1 行に置きます
	public bool $singleParameterOnOneLine = false;
	// クラスも関数も含まない名前空間を省きます
	public bool $omitEmptyNamespaces = true;
	// declare(strict_types) を <?php と同じ行に置きます
	public bool $declareOnOpenTag = false;
	// 関数とメソッドの閉じかっこと戻り値の型のあいだの区切り
	public string $returnTypeColon = ': ';
}

標準の PrinterPsrPrinter は、実際どこがどう違い、なぜ違うのでしょうか。なぜパッケージに printer が PsrPrinter ひとつだけではないのでしょうか。

標準の Printer は、私たちが Nette 全体でそうしているようにコードを整えます。Nette は PSR よりずっと前に生まれましたし、PSR の標準はしばしば遅れて(ときには PHP の新しい機能が入ってから何年も経って)出てきたので、Nette のコーディング規約はいくつかの細かな点で違います。いちばんの違いは空白ではなくタブを使うことです。プロジェクトでタブを使えば幅を変えられ、それが目の不自由な人には欠かせないと私たちは分かっています。細かな違いの例としては、関数とメソッドの開く波かっこをいつも別の行に置くことです。PSR のおすすめは私たちには筋が通らないものに見え、コードの見通しを悪くします

どの型も、合併や交差の型も、文字列として渡せます。ネイティブの型にはあらかじめ用意された定数も使えます。

use Nette\PhpGenerator\Type;

$member->setType('array'); // または Type::Array
$member->setType('?array'); // または Type::nullable(Type::Array)
$member->setType('array|string'); // または Type::union(Type::Array, Type::String)
$member->setType('Foo&Bar'); // または Type::intersection(Foo::class, Bar::class)
$member->setType(null); // 型を取り除きます

同じことが setReturnType() メソッドにも当てはまります。

リテラル

Literal を使うと、どんな PHP のコードでも渡せます。たとえばプロパティやパラメータの既定値にです。

use Nette\PhpGenerator\Literal;

$class = new Nette\PhpGenerator\ClassType('Demo');

$class->addProperty('foo', new Literal('Iterator::SELF_FIRST'));

$class->addMethod('bar')
	->addParameter('id', new Literal('1 + 2'));

echo $class;

結果:

class Demo
{
	public $foo = Iterator::SELF_FIRST;

	public function bar($id = 1 + 2)
	{
	}
}

Literal にパラメータを渡して、プレースホルダで正しい PHP のコードに整えさせることもできます。

new Literal('substr(?, ?)', [$a, $b]);
// たとえば次を生成します: substr('hello', 5)

新しいオブジェクトを作るリテラルは、new のメソッドで簡単に作れます。

Literal::new(Demo::class, [$a, 'foo' => $b]);
// たとえば次を生成します: new Demo(10, foo: 20)

アトリビュート

PHP 8 のアトリビュートは、すべてのクラス、メソッド、プロパティ、定数、enum、関数、クロージャ、パラメータに足せます。パラメータの値にはリテラルも使えます。

$class = new Nette\PhpGenerator\ClassType('Demo');
$class->addAttribute('Table', [
	'name' => 'user',
	'constraints' => [
		Literal::new('UniqueConstraint', ['name' => 'ean', 'columns' => ['ean']]),
	],
]);

$class->addProperty('list')
	->addAttribute('Deprecated');

$method = $class->addMethod('count')
	->addAttribute('Foo\Cached', ['mode' => true]);

$method->addParameter('items')
	->addAttribute('Bar');

echo $class;

結果:

#[Table(name: 'user', constraints: [new UniqueConstraint(name: 'ean', columns: ['ean'])])]
class Demo
{
	#[Deprecated]
	public $list;


	#[Foo\Cached(mode: true)]
	public function count(
		#[Bar]
		$items,
	) {
	}
}

プロパティフック

プロパティフック(PropertyHookクラスが表します)を使うと、PHP 8.4 で入った機能として、プロパティの get と set の操作を定められます。

$class = new Nette\PhpGenerator\ClassType('Demo');
$prop = $class->addProperty('firstName')
    ->setType('string');

$prop->addHook('set', 'strtolower($value)')
    ->addParameter('value')
	    ->setType('string');

$prop->addHook('get')
	->setBody('return ucfirst($this->firstName);');

echo $class;

これは次を生成します。

class Demo
{
    public string $firstName {
        set(string $value) => strtolower($value);
        get {
            return ucfirst($this->firstName);
        }
    }
}

プロパティとプロパティフックは abstract や final にできます。

$class->addProperty('id')
    ->setType('int')
    ->addHook('get')
        ->setAbstract();

$class->addProperty('role')
    ->setType('string')
    ->addHook('set', 'strtolower($value)')
        ->setFinal();

非対称の可視性

PHP 8.4 はプロパティの非対称の可視性を持ち込みます。読みと書きに違うアクセスの水準を設定できます。

可視性は、パラメータを 2 つ取る setVisibility() メソッドでも、setPublic()setProtected()setPrivate()mode のパラメータでその可視性が読みと書きのどちらに当たるかを指定しても設定できます。既定のモードは 'get' です。

$class = new Nette\PhpGenerator\ClassType('Demo');

$class->addProperty('name')
    ->setType('string')
    ->setVisibility('public', 'private'); // 読みは public、書きは private

$class->addProperty('id')
    ->setType('int')
    ->setProtected('set'); // 書きは protected

echo $class;

これは次を生成します。

class Demo
{
    public private(set) string $name;

    protected(set) int $id;
}

名前空間

クラス、トレイト、インターフェース、enum(以下ではクラスと呼びます)は、PhpNamespaceクラスが表す名前空間にまとめられます。

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');

// 名前空間の中に新しいクラスを作ります
$class = $namespace->addClass('Task');
$interface = $namespace->addInterface('Countable');
$trait = $namespace->addTrait('NameAware');

// あるいは既存のクラスや関数を名前空間へ入れます
$class = new Nette\PhpGenerator\ClassType('Task');
$namespace->add($class);

同じ名前のクラスがその名前空間にすでにあると、例外が投げられます。

use の句を定められます。

// use Http\Request;
$namespace->addUse(Http\Request::class);
// use Http\Request as HttpReq;
$namespace->addUse(Http\Request::class, 'HttpReq');
// use function iter\range;
$namespace->addUseFunction('iter\range');

定められた別名や今の名前空間をもとに、完全修飾のクラス名、関数名、定数名を短くするには simplifyName メソッドを使います。

echo $namespace->simplifyName('Foo\Bar'); // 'Bar'。'Foo' が今の名前空間だからです
echo $namespace->simplifyName('iter\range', $namespace::NameFunction); // 'range'。定められた use 文のおかげです

逆に、短くしたクラス名、関数名、定数名を完全修飾の名前に戻すには resolveName メソッドを使います。

echo $namespace->resolveName('Bar'); // 'Foo\Bar'
echo $namespace->resolveName('range', $namespace::NameFunction); // 'iter\range'

クラス名の解決

クラスが名前空間の一部なら、描かれ方が少し違います。 すべての型(たとえば型宣言、戻り値の型、親のクラス名、実装するインターフェース、使うトレイト、アトリビュート)が自動的に解決されます(下で説明するように切ることもできます)。つまり定義では完全修飾のクラス名を使わなければならず、できあがるコードではそれが(use の句をもとにした)別名や、(同じ名前空間なら)短くした名前に置き換わります。

$namespace = new Nette\PhpGenerator\PhpNamespace('Foo');
$namespace->addUse('Bar\AliasedClass');

$class = $namespace->addClass('Demo');
$class->addImplement('Foo\A') // A に短くなります
	->addTrait('Bar\AliasedClass'); // AliasedClass に短くなります

$method = $class->addMethod('method');
$method->addComment('@return ' . $namespace->simplifyType('Foo\D')); // コメントの中では手で短くします
$method->addParameter('arg')
	->setType('Bar\OtherClass'); // \Bar\OtherClass に変わります

echo $namespace;

// あるいは PSR-2 / PSR-12 / PER に沿った出力には PsrPrinter を使います
// echo (new Nette\PhpGenerator\PsrPrinter)->printNamespace($namespace);

結果:

namespace Foo;

use Bar\AliasedClass;

class Demo implements A
{
	use AliasedClass;

	/**
	 * @return D
	 */
	public function method(\Bar\OtherClass $arg)
	{
	}
}

自動の解決は次のように切れます。

$printer = new Nette\PhpGenerator\Printer; // または PsrPrinter
$printer->setTypeResolving(false);
echo $printer->printNamespace($namespace);

PHP のファイル

クラス、関数、名前空間は、PhpFileクラスが表す PHP のファイルにまとめられます。

$file = new Nette\PhpGenerator\PhpFile;
$file->addComment('This file is auto-generated.');
$file->setStrictTypes(); // declare(strict_types=1) を足します

$class = $file->addClass('Foo\A');
$function = $file->addFunction('Foo\foo');

// あるいは
// $namespace = $file->addNamespace('Foo');
// $class = $namespace->addClass('A');
// $function = $namespace->addFunction('foo');

echo $file;

// あるいは PSR-2 / PSR-12 / PER に沿った出力には PsrPrinter を使います
// echo (new Nette\PhpGenerator\PsrPrinter)->printFile($file);

結果:

<?php

/**
 * This file is auto-generated.
 */

declare(strict_types=1);

namespace Foo;

class A
{
}

function foo()
{
}

既存のクラス、関数、名前空間のオブジェクトも add() メソッドでファイルへ入れられます。

$file = new Nette\PhpGenerator\PhpFile;
$class = new Nette\PhpGenerator\ClassType('Demo');
$file->add($class);

注意してください。 関数、クラス、名前空間の外に、そのほかのコード(echo 'hello' など)をファイルへ足すことはできません。

既存の要素から生成する

上で説明した API でクラスと関数を組み立てるほかに、リフレクションを使って既存のものをもとに自動的に生成させることもできます。

// PDO クラスと同じクラスを作ります
$class = Nette\PhpGenerator\ClassType::from(PDO::class);

// trim() 関数と同じ関数を作ります
$function = Nette\PhpGenerator\GlobalFunction::from('trim');

// 渡されたクロージャをもとにクロージャを作ります
$closure = Nette\PhpGenerator\Closure::from(
	function (stdClass $a, $b = null) {},
);

既定では、関数とメソッドの本体は空です。それも読み込みたいなら、次のようにします(nikic/php-parser のパッケージが入っている必要があります)。

$class = Nette\PhpGenerator\ClassType::from(Foo::class, withBodies: true);

$function = Nette\PhpGenerator\GlobalFunction::from('foo', withBody: true);

PHP のファイルから読み込む

関数、クラス、インターフェース、enum は、PHP のコードを含む文字列から直接読み込むこともできます。たとえば ClassType のオブジェクトを作るには次のようにします。

$class = Nette\PhpGenerator\ClassType::fromCode(<<<XX
	<?php

	class Demo
	{
		public $foo;
	}
	XX);

PHP のコードからクラスを読み込むとき、メソッドの本体の外の 1 行のコメント(たとえばプロパティのもの)は無視されます。このライブラリにはそれを扱う API がないからです。

PHP のファイル全体を直接読み込むこともできます。そこにはいくつでもクラス、関数、さらには名前空間を入れられます。

$file = Nette\PhpGenerator\PhpFile::fromCode(file_get_contents('classes.php'));

ファイルの先頭のコメントと strict_types の宣言も読み込まれます。とはいえ、そのほかの大域のコードはすべて無視されます。

nikic/php-parser が入っている必要があります。

ファイルの大域のコードや、メソッドの本体の中の個々の文を扱う必要があるなら、nikic/php-parser のライブラリを直接使うほうがよいでしょう。

クラスの操作器

ClassManipulatorクラスは、クラスを操作する道具を差し出します。

$class = new Nette\PhpGenerator\ClassType('Demo');
$manipulator = new Nette\PhpGenerator\ClassManipulator($class);

inheritMethod() メソッドは、親のクラスや実装するインターフェースからメソッドをあなたのクラスへ写します。おかげでそのメソッドを上書きしたり、その見出しを広げたりできます。

$method = $manipulator->inheritMethod('bar');
$method->setBody('...');

inheritProperty() メソッドは、親のクラスからプロパティをあなたのクラスへ写します。同じプロパティを、たとえば違う既定値で持ちたいときに役立ちます。

$property = $manipulator->inheritProperty('foo');
$property->setValue('new value');

implement() メソッドは、渡されたインターフェースや抽象クラスのすべての抽象メソッドとプロパティを、あなたのクラスに自動的に実装します。

$manipulator->implement(SomeInterface::class);
// これであなたのクラスは SomeInterface を実装し、そのすべてのメソッドの骨組みを持ちます

変数のダンプ

Dumperクラスは、変数を読み解ける PHP のコードに変えます。標準の var_export() 関数より優れた、分かりやすい出力を与えます。

$dumper = new Nette\PhpGenerator\Dumper;

$var = ['a', 'b', 123];

echo $dumper->dump($var); // ['a', 'b', 123] を出力します

互換の表

PhpGenerator 4.2 は PHP 8.1 から 8.5 に対応しています。

新しい版へ上げるなら、アップグレードのページをご覧ください。

バージョン: 4.x