SmartObject

SmartObjectは長年にわたりPHPのオブジェクトの動作を改善してきました。PHP 8.4以降、その機能はすべてPHP自体の一部となり、PHPにおける現代的なオブジェクト指向アプローチの先駆者としての歴史的使命を終えました。

インストール:

composer require nette/utils

SmartObjectは、当時のPHPのオブジェクトモデルの欠点を解決する革新的なソリューションとして2007年に誕生しました。PHPが多くのオブジェクト設計の問題に苦しんでいた時代に、開発者の作業を大幅に改善し、簡素化しました。Netteフレームワークの伝説的な一部となりました。オブジェクトプロパティへのアクセス制御から洗練されたシンタックスシュガーまで、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の最も厄介な問題の1つは、オブジェクトを扱う際の一貫性のない動作でした。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以降、未宣言メンバーへのアクセスはエラーと見なされます。

ヘルプ “Did you mean?”

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には「did you mean?」のような形式はありませんが、この追記は Tracy によってエラーに追加できます。そして、そのようなエラーを 自動的に修正 することもできます。

アクセス制御付きプロパティ

SmartObjectがPHPにもたらした重要な革新は、アクセス制御付きプロパティでした。C#やPythonなどの言語で一般的なこの概念により、開発者はオブジェクトのデータへのアクセスをエレガントに制御し、その一貫性を確保できました。プロパティはオブジェクト指向プログラミングの強力なツールです。変数のように機能しますが、実際にはメソッド(ゲッターとセッター)によって表されます。これにより、入力を検証したり、読み取り時に値を生成したりできます。

プロパティを使用するには:

  • クラスに @property <type> $xyz の形式でアノテーションを追加します
  • getXyz() または isXyz() という名前のゲッター、setXyz() という名前のセッターを作成します
  • ゲッターとセッターが public または protected であることを確認します。これらはオプションです – したがって、read-only または write-only プロパティとして存在できます

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\Object は、getReflection() および 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以降、いわゆる「ファーストクラス呼び出し可能構文」:https://www.php.net/…lable_syntax を利用できます。これはこの概念をさらに推し進めます:

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

イベント

SmartObjectは、イベントを扱うための簡略化された構文を提供します。イベントにより、オブジェクトは自身の状態の変化についてアプリケーションの他の部分に通知できます:

class Circle extends Nette\Object
{
	public array $onChange = [];

	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.0