SmartObject

SmartObject は長年にわたって PHP のオブジェクトの振る舞いを強化してきました。PHP 8.4 以降、その機能はすべて PHP 自体のネイティブな一部になり、PHP における現代的なオブジェクト指向の先駆けという歴史的な使命を果たし終えました。

インストール:

composer require nette/utils

SmartObject は 2007 年に、当時の PHP のオブジェクトモデルの弱点に対する革新的な解として登場しました。PHP がオブジェクト指向の設計で数々の問題を抱えていた時代に、大きな改善をもたらし、開発者の作業を楽にしました。そして Nette Framework の伝説的な部品になりました。SmartObject は、PHP が何年もあとになってようやく手に入れる機能、つまりオブジェクトのプロパティへのアクセス制御から洗練されたシンタックスシュガーまでを提供していました。PHP 8.4 のリリースとともに、その機能のほとんどが言語のネイティブな一部となり、歴史的な使命を果たしました。PHP の歩みを実に 17 年も先取りしていたのです。

技術的には、SmartObject は興味深い変遷を辿りました。最初は Nette\Object クラスとして実装され、ほかのクラスがそこから必要な機能を継承していました。大きな変化は、トレイトのサポートを導入した PHP 5.4 とともに訪れました。これにより Nette\SmartObject トレイトへの変身が可能になり、柔軟さが増しました。すでに別のクラスを継承しているクラスでも、この機能を使えるようになったのです。もとの Nette\Object クラスは PHP 7.2(Object という語をクラス名に使うことを禁じました)とともに消えましたが、Nette\SmartObject トレイトは生き続けています。

Nette\Object、そしてのちの Nette\SmartObject が提供していた機能を見ていきましょう。そのどれもが、当時の PHP のオブジェクト指向プログラミングにとって大きな一歩でした。

一貫したエラーの状態

初期の PHP で最も切実だった問題のひとつが、オブジェクトを扱うときの一貫性のない振る舞いでした。Nette\Object はこの混沌に秩序と予測可能性をもたらしました。もともとの PHP の振る舞いを見てみましょう。

echo $obj->undeclared;    // E_NOTICE、のちに E_WARNING
$obj->undeclared = 1;     // 警告なしで黙って通る
$obj->unknownMethod();    // Fatal error(try/catch で捕まえられない)

致命的エラーは、何の対処もできないままアプリケーションを終わらせてしまいます。存在しないメンバーへの書き込みが警告なしに黙って通ることは、見つけにくい深刻なエラーにつながりかねません。Nette\Object はこれらの場合をすべて捕まえて MemberAccessException を投げ、プログラマーがエラーに反応して処理できるようにしました。

echo $obj->undeclared;   // Nette\MemberAccessException を投げる
$obj->undeclared = 1;    // Nette\MemberAccessException を投げる
$obj->unknownMethod();   // Nette\MemberAccessException を投げる

PHP 7.0 以降、この言語はもう捕まえられない致命的エラーを起こさず、PHP 8.2 以降、宣言されていないメンバーへのアクセスはエラーとみなされます。

「もしかして?」の助け

Nette\Object には、打ち間違いに対する賢い提案という、とても便利な機能がありました。開発者がメソッド名や変数名を間違えると、エラーを報告するだけでなく、正しい名前を提案して助けてくれたのです。「did you mean?」として知られるこの象徴的なメッセージは、プログラマーが打ち間違いを探す何時間もの手間を省きました。

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// Nette\MemberAccessException を投げる
// "Call to undefined static method Foo::form(), did you mean from()?"

今の PHP には「もしかして?」にあたるものはありませんが、Tracyがこの一文をエラーに付け足せます。しかもそうしたエラーを自動的に直すことさえできます。

アクセスを制御できるプロパティ

SmartObject が PHP にもたらした重要な革新のひとつが、アクセスを制御できるプロパティです。C# や Python のような言語ではおなじみのこの考え方によって、開発者はオブジェクトのデータへのアクセスをきれいに制御し、その一貫性を保てるようになりました。プロパティはオブジェクト指向プログラミングの強力な道具です。変数のように振る舞いますが、実際にはメソッド(ゲッターとセッター)で表されます。おかげで入力の検証や、読み取り時の値の生成ができます。

プロパティを使うには次のことが必要でした。

  • クラスにアノテーション @property <type> $xyz を付ける
  • getXyz() または isXyz() という名前のゲッター、setXyz() という名前のセッターを作る
  • ゲッターとセッターを publicprotected にする。どちらも省略できるので、読み取り専用書き込み専用のプロパティも作れました

Circle クラスを使った実践的な例を見てみましょう。半径が常に非負であることを保証するためにプロパティを使います。public $radius をプロパティに置き換えます。

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // public ではありません!

	// $radius プロパティのゲッター
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// $radius プロパティのセッター
	protected function setRadius(float $radius): void
	{
		// 保存する前に値を整えます
		$this->radius = max(0.0, $radius);
	}

	// $visible プロパティのゲッター
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // 実際には setRadius(10) を呼びます
echo $circle->radius;  // getRadius() を呼びます
echo $circle->visible; // isVisible() を呼びます

PHP 8.4 以降は、同じ機能をプロパティフックで実現でき、ずっときれいで簡潔な構文になります。

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

拡張メソッド

Nette\Object は、現代的なプログラミング言語に着想を得たもうひとつの興味深い考え方を PHP に持ち込みました。拡張メソッドです。C# から借りてきたこの機能により、開発者は既存のクラスを変更したり継承したりせずに、新しいメソッドできれいに拡張できました。たとえば、独自の DateTimePicker を追加する addDateTime() メソッドをフォームに足せます。

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

拡張メソッドは、その名前がコードエディタで補完されず、むしろメソッドが存在しないと報告されてしまうため、実用的ではないと分かりました。そのためサポートは打ち切られました。今日では、クラスの機能を拡張するにはコンポジションや継承を使うのが一般的です。

クラス名の取得

SmartObject にはクラス名を取得する簡単なメソッドがありました。

$class = $obj->getClass(); // Nette\Object を使う場合
$class = $obj::class;      // PHP 8.0 以降

リフレクションとアノテーションへのアクセス

Nette\ObjectgetReflection()getAnnotation() メソッドを通じて、リフレクションとアノテーションへのアクセスを提供していました。この方法はクラスのメタ情報の扱いを大きく簡単にしました。

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // 'John Doe' を返す

PHP 8.0 以降は属性を通じてメタ情報にアクセスでき、より多くのことができて型のチェックも優れています。

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

メソッドのゲッター

Nette\Object は、メソッドをまるで変数のように渡すきれいな方法を提供していました。

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5

PHP 8.1 以降は ファーストクラス callable 構文 が使え、この考え方をさらに推し進めています。

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

イベント

SmartObject はイベントを扱うための簡潔な構文を提供します。イベントを使うと、オブジェクトが自分の状態の変化をアプリケーションのほかの部分に知らせられます。

class Circle
{
	use Nette\SmartObject;

	public array $onChange = [];

	private float $radius = 0.0;

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

$this->onChange($this, $radius) というコードは、次のループと同じです。

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

分かりやすさのために、マジックな $this->onChange() メソッドは避けることをおすすめします。実用的な代わりが Nette\Utils\Arrays::invoke 関数です。

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
バージョン: 4.x