Create WeakInterface
authorRobert Sesek <rsesek@bluestatic.org>
Sat, 6 Aug 2011 19:14:57 +0000 (15:14 -0400)
committerRobert Sesek <rsesek@bluestatic.org>
Sat, 6 Aug 2011 19:14:57 +0000 (15:14 -0400)
base/weak_interface.php [new file with mode: 0644]
testing/tests/base/weak_interface_test.php [new file with mode: 0644]

diff --git a/base/weak_interface.php b/base/weak_interface.php
new file mode 100644 (file)
index 0000000..1f644a4
--- /dev/null
@@ -0,0 +1,154 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+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 (file)
index 0000000..e547ff7
--- /dev/null
@@ -0,0 +1,127 @@
+<?php
+// Hoplite
+// Copyright (c) 2011 Blue Static
+// 
+// This program is free software: you can redistribute it and/or modify it
+// under the terms of the GNU General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or any later version.
+// 
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+// FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
+// more details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+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";
+    
+  }
+}