From f4e709e4f0dfd73a32b20c151a6fe9508fe1a96b Mon Sep 17 00:00:00 2001 From: Robert Sesek Date: Sat, 6 Aug 2011 15:14:57 -0400 Subject: [PATCH] Create WeakInterface --- base/weak_interface.php | 154 +++++++++++++++++++++ testing/tests/base/weak_interface_test.php | 127 +++++++++++++++++ 2 files changed, 281 insertions(+) create mode 100644 base/weak_interface.php create mode 100644 testing/tests/base/weak_interface_test.php diff --git a/base/weak_interface.php b/base/weak_interface.php new file mode 100644 index 0000000..1f644a4 --- /dev/null +++ b/base/weak_interface.php @@ -0,0 +1,154 @@ +. + +namespace hoplite\base { + +/*! + A WeakInterface is a class that implements some interface and then can bind + a subset of those methods to some other object. This allows a delegate pattern + to be implemented more easily in that the delegate class need not implement + every method. + + To use this class, define an interface as you normally would. When making a + call to an object that implements the interface, create a WeakInterface object + and instead make the calls on that. +*/ +class WeakInterface +{ + /*! @var array Map of MethodImps keyed by the method name. */ + private $imps = array(); + + /*! @var object The object to which this interface is bound. */ + private $object = NULL; + + /*! @var bool Whether or not a NULL object will throw when called. */ + private $null_allowed = FALSE; + + /*! Creates a weak interface with the name of an actual interface. */ + public function __construct($interface) + { + $reflector = new \ReflectionClass($interface); + $methods = $reflector->GetMethods(); + foreach ($methods as $method) { + $name = $method->name; + $this->imps[$name] = new internal\MethodImp($this, $method); + } + } + + /*! Gets the object to which this interface is bound. */ + public function get() + { + return $this->object; + } + + /*! Sets whether NULL is allowed. */ + public function set_null_allowed($flag) + { + $this->null_allowed = $flag; + } + + /*! Binds an object to the interface. */ + public function Bind($object) + { + $this->_CheckObject($object); + $this->object = $object; + } + + /*! Magic method that performs the actual method invocation. */ + public function __call($meth, $args) + { + if (!$this->object) { + if (!$this->null_allowed) + throw new WeakInterfaceException('NULL object when calling ' . $meth); + else + return; + } + + return $this->imps[$meth]->Invoke($args); + } + + /*! Ensures that the bound object properly implements the interface. */ + private function _CheckObject($object) + { + $reflector = new \ReflectionClass($object); + $methods = $reflector->GetMethods(); + $by_name = array(); + foreach ($methods as $method) { + $by_name[$method->name] = $method; + } + + foreach ($this->imps as $name => $imp) { + if ($imp->IsRequired() && !isset($by_name[$name])) + throw new WeakInterfaceException($reflector->name . ' does not implement required interface method ' . $name); + + if (isset($by_name[$name]) && !$imp->Matches($by_name[$name])) + throw new WeakInterfaceException($reflector->name . '::' . $name . ' does not match interface definition'); + } + } +} + +class WeakInterfaceException extends \Exception {} + +} // namespace hoplite\base + +namespace hoplite\base\internal { + +/*! + An encapsulation of a method implementation. +*/ +class MethodImp +{ + /*! @var WeakInterface The interface to which this method belongs. */ + private $interface = NULL; + + /*! @var ReflectionMethod The method of the actual interface that this is implementing. */ + private $method = NULL; + + public function __construct(\hoplite\base\WeakInterface $interface, + \ReflectionMethod $method) + { + $this->interface = $interface; + $this->method = $method; + } + + /*! Forwards a method call. */ + public function Invoke($args) + { + $reflector = new \ReflectionClass($this->interface->get()); + try { + $impl = $reflector->GetMethod($this->method->name); + } catch (\ReflectionException $e) { + if ($this->IsRequired()) + throw $e; + return; + } + return $impl->InvokeArgs($this->interface->get(), $args); + } + + /*! Performs method signature validation. */ + public function Matches(\ReflectionMethod $method) + { + return $method->GetParameters() == $this->method->GetParameters(); + } + + /*! Checks if a method is marked as required. */ + public function IsRequired() + { + return strpos($this->method->GetDocComment(), '@required') !== FALSE; + } +} + +} // namespace hoplite\base\internal diff --git a/testing/tests/base/weak_interface_test.php b/testing/tests/base/weak_interface_test.php new file mode 100644 index 0000000..e547ff7 --- /dev/null +++ b/testing/tests/base/weak_interface_test.php @@ -0,0 +1,127 @@ +. + +namespace hoplite\test; +use \hoplite\base as base; + +require_once HOPLITE_ROOT . '/base/weak_interface.php'; + +interface AllOptional +{ + public function DoSomething(); + public function DoSomething1(array $a); +} + +class AllOptionalImp +{ + public $do_something = FALSE; + + public function DoSomething() + { + $this->do_something = TRUE; + return 'foo'; + } +} + +class AllOptionalImpBad +{ + public function DoSomething1($a, $b) {} +} + +interface OneRequired +{ + public function DoSomething(); + + /** @required */ + public function DoRequired(); +} + +class OneRequiredImp +{ + public $do_required = FALSE; + + public function DoRequired() + { + $this->do_required = TRUE; + } +} + +class WeakInterfaceTest extends \PHPUnit_Framework_TestCase +{ + public function testAllOptional() + { + $delegate = new base\WeakInterface('hoplite\test\AllOptional'); + $delegate->Bind(new AllOptionalImp); + $this->assertEquals('foo', $delegate->DoSomething()); + $this->assertTrue($delegate->get()->do_something); + $delegate->DoSomething1(); + } + + public function testOneRequired() + { + $delegate = new base\WeakInterface('hoplite\test\OneRequired'); + $delegate->Bind(new OneRequiredImp); + $delegate->DoRequired(); + $this->assertTrue($delegate->get()->do_required); + $delegate->DoSomething(); + } + + public function testRequirements() + { + $this->setExpectedException('hoplite\base\WeakInterfaceException'); + $delegate = new base\WeakInterface('hoplite\test\OneRequired'); + $delegate->Bind(new AllOptionalImp); + } + + public function testNull() + { + $this->setExpectedException('hoplite\base\WeakInterfaceException'); + $delegate = new base\WeakInterface('hoplite\test\AllOptional'); + $delegate->DoSomething(); + } + + public function testNullAllowed() + { + $delegate = new base\WeakInterface('hoplite\test\AllOptional'); + $delegate->set_null_allowed(TRUE); + $delegate->DoSomething(); + } + + public function testMethodSignatures() + { + $this->setExpectedException('hoplite\base\WeakInterfaceException'); + $delegate = new base\WeakInterface('hoplite\test\AllOptional'); + $delegate->Bind(new AllOptionalImpBad); + } + + public function testTiming() + { + $delegate = new base\WeakInterface('hoplite\test\AllOptional'); + $imp = new AllOptionalImp; + $delegate->Bind($imp); + + $mt_s = microtime(TRUE); + $delegate->DoSomething(); + $mt_e = microtime(TRUE); + print 'WeakInterface: ' . ($mt_e - $mt_s) . 'µs' . "\n"; + + $mt_s = microtime(TRUE); + $imp->DoSomething(); + $mt_e = microtime(TRUE); + print 'Straight Call: ' . ($mt_e - $mt_s) . 'µs' . "\n"; + + } +} -- 2.22.5