概要
プロキシを挟んでメソッドを実行することで処理の主体を切り替え、状況に応じたメソッドのラッピングを簡単にします。
// 普通に走る
echo $dog->run(); // => "1"
// 脚の性能 ($dog固有の値) に依存して走る
echo $dog->withLegs()->run(); // => "4"
// 吠える
$dog->bark(); // => "わん"
// 3回吠える
$dog->repeat(3)->bark(); // => "わんわんわん"
// 別のオブジェクト、別のメソッドでも使える
$cat->repeat(2)->mew(); // => "にゃーにゃー"
// お腹が空いているのなら食べる
$dog->when(function ($dog) {
return $dog->isHungry();
})->eat(); // => "ばくばく"
// もうお腹がいっぱいになったので食べない
$dog->when(function ($dog) {
return $dog->isHungry();
})->eat(); // => eat() is not executed.
// public でないメソッド (Dog::grow()) を実行
echo $dog->getAge(); // => "0"
try {
$dog->grow();
} catch (\Throwable $e) {
echo $e->getMessage(); // => "Call to private method Dog::grow() from context ''"
}
$dog->unprotect()->grow();
echo $dog->getAge(); // => "1"
サンプルコード
ここで紹介するコードのソースは GitHub に上げてあります。動作を確かめたい場合は git clone
して composer install
してから実行してください (PHP 7.1以上) 。
git clone git@github.com:KNJ/php-object-proxy.git
cd php-object-proxy
composer install
./vendor/bin/phpunit tests
サンプルの実行コマンド
php running.php
php repeating.php
php judgement.php
php unprotecting.php
./vendor/bin/phpunit tests
※ 序論が長いので手っ取り早く実装を知りたい方は応用例とサンプルのソースコードを参照してください。
メソッドの挙動を変更するときの普通の方法
あるメソッドのふるまいを少しだけ変えたい場合には引数を与えてパラメータの変更ができるようにするでしょう。
例: Animal::run()
はスピードを変更して実行できる。
<?php
namespace Sample\Running;
class Animal
{
public function run($speed = 1)
{
// run at a speed of $speed
return $speed;
}
}
また、継承したクラスに依存するふるまいを加えたい場合にはオーバーライドをすることでしょう。
例: Dog
は足の数だけ速くなる (現実はそうではないと思いますが) 。
<?php
namespace Sample\Running;
class Dog extends Animal
{
const LEGS = 4;
public function run($speed = 1)
{
$speed = $speed * self::LEGS;
return parent::run($speed);
}
}
では、同じように Human
というクラスも Animal
を継承して作ってみましょう。
例: Human
は足の数だけ速くなる。
<?php
namespace Sample\Running;
class Human extends Animal
{
const LEGS = 2;
public function run($speed = 1)
{
$speed = $speed * self::LEGS;
return parent::run($speed);
}
}
おや? Dog
のコピペみたいですね。この調子で Cat
とか Bear
とか作ったらえらいことになりそうです…。であれば Animal::run()
は最初から LEGS
に依存させましょうか?しかし、 Animal
が哺乳類以外も含めるのであれば Snake
はどうすれば… (そもそも"走る"のでしょうか?) 。よし、 Animal
を継承した Mammal
を作って run()
をオーバーライドし、 Mammal
を継承した Dog
や Human
を作りましょう!でも多重継承って、本当にいいんでしょうか?う~ん…。あー。 (ここで腕を組んで目をつむる)
メソッドをまったく別のオブジェクトに委譲してしまうという方法
悩んでいても埒が明かないので、 run()
は LEGS
を利用する場合と利用しない場合があると視点を変えてみましょう。脚があってもそれを使わずに移動する動物もいます (小学生の時に観察したハナムグリの幼虫には大変な関心を抱いたものです) 。 LEGS
を利用する・しないは選択できるようにしましょう。そして、 LEGS
を利用して run()
する場合、 Legs
というクラスに run()
を代理させます。
もう少しわかりやすく説明しましょう。犬がただ走るのであればそれは「犬が走る」となります。しかし、犬が脚を使って走るのであれば主体は脚に切り替わり「脚が走る」ということになります。このことをコードに書き出してみましょう。
まず、脚があるやつとないやつを区別しなければならないので次のようなインターフェイスを用意します。
<?php
namespace Sample\Running;
interface WithLegs
{
// legs() は脚の数を返します.
public function legs(): int;
}
脚は__主体を代理する__ことと、__走る__ことができるのでその2つのメソッドを用意します。当然主体は脚を有していることが前提なので WithLegs
を実装したオブジェクトしか代理できません。このような代理をするオブジェクトをプロキシと呼ぶことにします。
<?php
namespace Sample\Running;
final class Legs
{
private $animal;
// represent は主体のオブジェクトを代理します.
public function represent(WithLegs $animal)
{
$this->animal = $animal;
return $this;
}
// これが実質 Animal::run() のオーバーライドになります.
public function run($speed = 1)
{
$legs = $this->animal->legs();
return $this->animal->run($speed * $legs);
}
}
脚を使って走れるやつは CanRunWithLegs
というトレイトを導入します。 withLegs()
というメソッドによりプロキシ化します。
<?php
namespace Sample\Running;
trait CanRunWithLegs
{
public function withLegs()
{
$proxy = new Legs;
return $proxy->represent($this);
}
}
そしてインターフェイスを実装した Dog
に変更します。
<?php
namespace Sample\Running;
class Dog extends Animal implements WithLegs
{
use CanRunWithLegs;
const LEGS = 4;
public function legs(): int
{
return self::LEGS;
}
}
さあ、実際に使ってみましょう!
<?php
require_once __DIR__.'/vendor/autoload.php';
$dog = new \Sample\Running\Dog;
// 犬が走る
echo $dog->run() // => "1"
.PHP_EOL;
// 犬の脚が走る
echo $dog->withLegs()->run() // => "4"
.PHP_EOL;
メソッドの挙動を変更する方法として、以上のように引数の変更や継承してオーバーライドする以外の方法を示しました。こうすることのメリットは、オリジナルのメソッドも呼び出せたり、オリジナルを上書きしていることがわかりやすいことです。
プロキシ化するとプロキシに定義されているメソッド以外は使えなくなってしまいます (まったく別のオブジェクトなのだから当然) 。以下の応用例ではマジックメソッド __call()
を使ってそれを解決します。
応用例
指定した回数だけ繰り返しメソッドを実行する
<?php
require_once __DIR__.'/vendor/autoload.php';
$dog = new \Sample\Repeating\Dog;
$cat = new \Sample\Repeating\Cat;
echo $dog->bark() // => "わん"
.PHP_EOL;
echo $dog->repeat(3)->bark() // => "わんわんわん"
.PHP_EOL;
echo $cat->repeat(2)->mew() // => "にゃーにゃー"
.PHP_EOL;
プロキシに引数を持たせて挙動を細かく制御し、さらに __call()
を実装してあらゆるメソッドをプロキシが実行できるようにしています。またトレイトさえ導入すればどんなオブジェクトでもプロキシ化できます1。
プロキシの実装:
<?php
namespace Sample\Repeating;
final class Repeater
{
private $original;
private $times;
// represent は主体のオブジェクトを代理します.
public function represent($original, $times)
{
$this->original = $original;
$this->times = $times;
return $this;
}
public function __call($method, $args)
{
for ($i = 0; $i < $this->times; $i++) {
$this->original->$method(...$args);
}
}
}
条件にマッチしたらメソッドを実行する
<?php
require_once __DIR__.'/vendor/autoload.php';
$dog = new \Sample\Judgement\Dog;
$dog->when(function ($dog) {
return $dog->isHungry();
})->eat(); // => ばくばく
$dog->when(function ($dog) {
return $dog->isHungry();
})->eat(); // => eat() is not executed.
if
を使いたくない人向け。上記の例では示せていませんが、一時的な変数を使用してもそのスコープをクロージャ内に閉じ込められるというメリットがあります。
プロキシの実装:
<?php
namespace Sample\Judgement;
use Closure;
final class Judge
{
private $original;
private $closure;
// represent は主体のオブジェクトを代理します.
public function represent($original, Closure $closure)
{
$this->original = $original;
$this->closure = $closure;
return $this;
}
public function __call($method, $args)
{
$closure = $this->closure;
if ($closure($this->original)) {
$this->original->$method(...$args);
}
}
}
public でないメソッドを外部から実行する
<?php
require_once __DIR__.'/vendor/autoload.php';
$dog = new \Sample\Unprotecting\Dog;
echo $dog->getAge().PHP_EOL; // => "0"
try {
$dog->grow();
} catch (\Throwable $e) {
echo $e->getMessage() // => "Call to private method Sample\Unprotecting\Dog::grow() from context ''"
.PHP_EOL;
}
$dog->unprotect()->grow();
echo $dog->getAge().PHP_EOL; // => "1"
$dog->unprotect()->grow(2);
echo $dog->getAge().PHP_EOL; // => "3"
ユニットテストとかで private or protected なメソッドをテストしたいときの黒魔術詠唱をプロキシで簡略化しちゃいます。
プロキシの実装:
<?php
namespace Sample\Unprotecting;
final class Unprotector
{
private $original;
// represent は主体のオブジェクトを代理します.
public function represent($original)
{
$this->original = $original;
return $this;
}
public function __call($method, $args)
{
$closure = function ($original, $method, $args) {
$original->$method(...$args);
};
$fn = $closure->bindTo($this->original, $this->original);
return $fn($this->original, $method, $args);
}
}
参考: PHP: Closure::bindTo - Manual
ただしこれをテストで使いたい場合、 トレイトが導入されていることが前提になるため、テストのためだけに多くのクラスで use trait 宣言をすることになってしまいます。このような場合はヘルパー関数でプロキシ化する手段を用意しておくとよいでしょう。
<?php
require_once __DIR__.'/../vendor/autoload.php';
// public でないメソッドにアクセス可能にするプロキシ化
function unprotect($original)
{
$proxy = new class($original)
{
public function __construct($original)
{
$this->original = $original;
}
public function __call($method, $args)
{
$closure = function ($original, $method, $args) {
$original->$method(...$args);
};
$fn = $closure->bindTo($this->original, $this->original);
return $fn($this->original, $method, $args);
}
};
return $proxy;
}
テストコード: unprotect($this->dog)->grow()
という形で使います。
<?php
declare(strict_types=1);
namespace Tests\Unprotecting;
use PHPUnit\Framework\TestCase;
final class DogTest extends TestCase
{
public function setUp()
{
$this->dog = new \Sample\Unprotecting\Dog;
}
public function testGrowMethod()
{
// Dog::$age を参照可能にする.
$age = new \ReflectionProperty(\Sample\Unprotecting\Dog::class, 'age');
$age->setAccessible(true);
// private なメソッド Dog::grow() を3回実行して
// Dog::$age が 0 -> 1 -> 2 -> 3 と増えることをテストする.
for ($i = 0; $i < 3; $i++) {
$this->assertSame($i, $age->getValue($this->dog));
unprotect($this->dog)->grow();
}
$this->assertSame(3, $age->getValue($this->dog));
}
}
- https://github.com/KNJ/php-object-proxy/blob/master/tests/bootstrap.php
- https://github.com/KNJ/php-object-proxy/blob/master/tests/Unprotecting/DogTest.php
他の応用アイデア
- メソッドの返り値をいろいろなフォーマットに変換する (JSON, シリアル化, ハッシュ値, 暗号化など)
- メソッド実行中に発行されたSQLを出力する
- メソッドの実行時間やメモリ使用量を計測する
など、応用できる範囲はとても広いと思います。他にもアイデアがあったらコメントをください!
余談
Laravel には HigherOrderCollectionProxy という、今回解説したプロキシ設計に似たような概念が Collection 操作のために実装されています。each
や map
などのメソッドをプロパティとして呼び出すとプロキシ化し、要素の1つ1つが次に呼び出すメソッドの主体になり反復処理が行われます。
実例を見たほうが早い:
- Laravel 5.4 – Higher Order Messaging for Collections - Laravel News
- 【5.5対応】Laravel の Collection を使い倒してみたくなった 〜 サンプルコード 115 連発 3/3 - Qiita
- Laravel 5.4で追加された「Higher Order Messages」がすごい - Qiita
次回予告 (近日予定)
さらに実用的な応用例として次回は Laravel における Repository パターンとキャッシュの連携を紹介したいと思います。
// UserRepository から "KNJ" という名前のユーザー情報を取得する.
$user = $userRepo->findByName('KNJ');
// UserRepository から "KNJ" という名前のユーザー情報を取得したときの結果をキャッシュする.
$userRepo->saveCache()->findByName('KNJ');
// UserRepository のキャッシュから "KNJ" という名前のユーザー情報を取得する.
$user = $userRepo->useCache()->findByName('KNJ');
// UserRepository から "KNJ" という名前のユーザー情報を取得したときの結果のキャッシュを消去する.
$userRepo->deleteCache()->findByName('KNJ');
-
制限を設けたい場合は
sample/Running/Legs.php
のようにrepresent()
の仮引数をインターフェイスでタイプヒントすればよいでしょう。 ↩