diff --git a/src/CallbackHandler.php b/src/CallbackHandler.php index 19f09b937..bdb830574 100644 --- a/src/CallbackHandler.php +++ b/src/CallbackHandler.php @@ -24,6 +24,7 @@ namespace Zend\Stdlib; use Closure, + ReflectionClass, WeakRef; /** @@ -46,21 +47,10 @@ class CallbackHandler protected $callback; /** - * @var string Event to which this handle is subscribed - */ - protected $event; - - /** - * Until callback has been validated, mark as invalid - * @var bool - */ - protected $isValidCallback = false; - - /** - * Callback options, if any + * Callback metadata, if any * @var array */ - protected $options; + protected $metadata; /** * Constructor @@ -70,10 +60,9 @@ class CallbackHandler * @param array $options Options used by the callback handler (e.g., priority) * @return void */ - public function __construct($event, $callback, array $options = array()) + public function __construct($callback, array $metadata = array()) { - $this->event = $event; - $this->options = $options; + $this->metadata = $metadata; $this->registerCallback($callback); } @@ -85,20 +74,25 @@ public function __construct($event, $callback, array $options = array()) * * If a callback is a functor, or an array callback composing an object * instance, this method will pass the object to a WeakRef instance prior - * to registering the callback. See {@link isValid()} for more information - * on how this affects execution. + * to registering the callback. * * @param callback $callback * @return void */ protected function registerCallback($callback) { - // If pecl/weakref is not installed, simply register it - if (!class_exists('WeakRef', false)) { + if (!is_callable($callback)) { + throw new Exception\InvalidCallbackException('Invalid callback provided; not callable'); + } + + // If pecl/weakref is not installed, simply store the callback and return + if (!class_exists('WeakRef')) { $this->callback = $callback; return; } + // If WeakRef exists, we want to use it. + // If we have a non-closure object, pass it to WeakRef, and then // register it. if (is_object($callback) && !$callback instanceof Closure) { @@ -127,46 +121,38 @@ protected function registerCallback($callback) $this->callback = array($target, $method); } - /** - * Get event to which handler is subscribed - * - * @return string - */ - public function getEvent() - { - return $this->event; - } - /** * Retrieve registered callback * * @return Callback - * @throws Exception\InvalidCallbackException If callback is invalid */ public function getCallback() { - if (!$this->isValid()) { - throw new Exception\InvalidCallbackException('Invalid callback provided; not callable'); - } - $callback = $this->callback; + + // String callbacks -- simply return if (is_string($callback)) { return $callback; } + // WeakRef callbacks -- pull it out of the object and return it if ($callback instanceof WeakRef) { return $callback->get(); } + // Non-WeakRef object callback -- return it if (is_object($callback)) { return $callback; } + // Array callback with WeakRef object -- retrieve the object first, and + // then return list($target, $method) = $callback; if ($target instanceof WeakRef) { return array($target->get(), $method); } + // Otherwise, return it return $callback; } @@ -179,187 +165,73 @@ public function getCallback() public function call(array $args = array()) { $callback = $this->getCallback(); - return call_user_func_array($callback, $args); - } - /** - * Get all callback options - * - * @return array - */ - public function getOptions() - { - return $this->options; - } + $isPhp54 = version_compare(PHP_VERSION, '5.4.0rc1', '>='); - /** - * Retrieve a single option - * - * @param string $name - * @return mixed - */ - public function getOption($name) - { - if (array_key_exists($name, $this->options)) { - return $this->options[$name]; + // Minor performance tweak; use call_user_func() until > 3 arguments + // reached + switch (count($args)) { + case 0: + if ($isPhp54) { + return $callback(); + } + return call_user_func($callback); + case 1: + if ($isPhp54) { + return $callback(array_shift($args)); + } + return call_user_func($callback, array_shift($args)); + case 2: + $arg1 = array_shift($args); + $arg2 = array_shift($args); + if ($isPhp54) { + return $callback($arg1, $arg2); + } + return call_user_func($callback, $arg1, $arg2); + case 3: + $arg1 = array_shift($args); + $arg2 = array_shift($args); + $arg3 = array_shift($args); + if ($isPhp54) { + return $callback($arg1, $arg2, $arg3); + } + return call_user_func($callback, $arg1, $arg2, $arg3); + default: + return call_user_func_array($callback, $args); } - return null; } /** - * Is the composed callback valid? - * - * Typically, this method simply checks to see if we have a valid callback. - * In a few situations, it does more. - * - * * If we have a string callback, we pass execution to - * {@link validateStringCallback()}. - * * If we have an object callback, we test to see if that object is a - * WeakRef {@see http://pecl.php.net/weakref}. If so, we return the value - * of its valid() method. Otherwise, we return the result of is_callable(). - * * If we have a callback array with a string in the first position, we - * pass execution to {@link validateArrayCallback()}. - * * If we have a callback array with an object in the first position, we - * test to see if that object is a WeakRef (@see http://pecl.php.net/weakref). - * If so, we return the value of its valid() method. Otherwise, we return - * the result of is_callable() on the callback. - * - * WeakRef is used to allow listeners to go out of scope. This functionality - * is turn-key if you have pecl/weakref installed; otherwise, you will have - * to manually remove listeners before destroying an object referenced in a - * listener. - * - * @return bool + * Invoke as functor + * + * @return mixed */ - public function isValid() + public function __invoke() { - // If we've already tested this, we can move on. Note: if a callback - // composes a WeakRef, this will never get set, and thus result in - // validation on each call. - if ($this->isValidCallback) { - return $this->callback; - } - - $callback = $this->callback; - - if (is_string($callback)) { - return $this->validateStringCallback($callback); - } - - if ($callback instanceof WeakRef) { - return $callback->valid(); - } - - if (is_object($callback) && is_callable($callback)) { - $this->isValidCallback = true; - return true; - } - - if (!is_array($callback)) { - return false; - } - - list($target, $method) = $callback; - if ($target instanceof WeakRef) { - if (!$target->valid()) { - return false; - } - $target = $target->get(); - return is_callable(array($target, $method)); - } - return $this->validateArrayCallback($callback); + return $this->call(func_get_args()); } /** - * Validate a string callback - * - * Check first if the string provided is callable. If not see if it is a - * valid class name; if so, determine if the object is invokable. + * Get all callback metadata * - * @param string $callback - * @return bool + * @return array */ - protected function validateStringCallback($callback) + public function getMetadata() { - if (is_callable($callback)) { - $this->isValidCallback = true; - return true; - } - - if (!class_exists($callback)) { - return false; - } - - // check __invoke before instantiating - if (!method_exists($callback, '__invoke')) { - return false; - } - $object = new $callback(); - - $this->callback = $object; - $this->isValidCallback = true; - return true; + return $this->metadata; } /** - * Validate an array callback + * Retrieve a single metadatum * - * @param array $callback - * @return bool + * @param string $name + * @return mixed */ - protected function validateArrayCallback(array $callback) + public function getMetadatum($name) { - $context = $callback[0]; - $method = $callback[1]; - - if (is_string($context)) { - // Dealing with a class/method callback, and class provided is a string classname - - if (!class_exists($context)) { - return false; - } - - // We need to determine if we need to instantiate the class first - $r = new \ReflectionClass($context); - if (!$r->hasMethod($method)) { - // Explicit method does not exist - if (!$r->hasMethod('__callStatic') && !$r->hasMethod('__call')) { - return false; - } - - if ($r->hasMethod('__callStatic')) { - // We have a __callStatic defined, so the original callback is valid - $this->isValidCallback = true; - return $callback; - } - - // We have __call defined, so we need to instantiate the class - // first, and redefine the callback - $object = new $context(); - $this->callback = array($object, $method); - $this->isValidCallback = true; - return $this->callback; - } - - // Explicit method exists - $rMethod = $r->getMethod($method); - if ($rMethod->isStatic()) { - // Method is static, so original callback is fine - $this->isValidCallback = true; - return $callback; - } - - // Method is an instance method; instantiate object and redefine callback - $object = new $context(); - $this->callback = array($object, $method); - $this->isValidCallback = true; - return $this->callback; - } elseif (is_callable($callback)) { - // The - $this->isValidCallback = true; - return $callback; + if (array_key_exists($name, $this->metadata)) { + return $this->metadata[$name]; } - - return false; + return null; } } diff --git a/test/CallbackHandlerTest.php b/test/CallbackHandlerTest.php index 32a815eae..b8acd085d 100644 --- a/test/CallbackHandlerTest.php +++ b/test/CallbackHandlerTest.php @@ -41,82 +41,106 @@ public function setUp() } } - public function testGetEventShouldReturnEvent() + public function testCallbackShouldStoreMetadata() { - $handler = new CallbackHandler('foo', 'rand'); - $this->assertEquals('foo', $handler->getEvent()); + $handler = new CallbackHandler('rand', array('event' => 'foo')); + $this->assertEquals('foo', $handler->getMetadatum('event')); + $this->assertEquals(array('event' => 'foo'), $handler->getMetadata()); } public function testCallbackShouldBeStringIfNoHandlerPassedToConstructor() { - $handler = new CallbackHandler('foo', 'rand'); + $handler = new CallbackHandler('rand'); $this->assertSame('rand', $handler->getCallback()); } public function testCallbackShouldBeArrayIfHandlerPassedToConstructor() { - $handler = new CallbackHandler('foo', array('\\ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test')); - $this->assertSame(array('\\ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test'), $handler->getCallback()); + $handler = new CallbackHandler(array('ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test')); + $this->assertSame(array('ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test'), $handler->getCallback()); } public function testCallShouldInvokeCallbackWithSuppliedArguments() { - $handler = new CallbackHandler('foo', array( $this, 'handleCall' )); + $handler = new CallbackHandler(array( $this, 'handleCall' )); $args = array('foo', 'bar', 'baz'); $handler->call($args); $this->assertSame($args, $this->args); } - public function testPassingInvalidCallbackShouldRaiseInvalidCallbackExceptionDuringCall() + public function testPassingInvalidCallbackShouldRaiseInvalidCallbackExceptionDuringInstantiation() { $this->setExpectedException('Zend\Stdlib\Exception\InvalidCallbackException'); - $handler = new CallbackHandler('Invokable', 'boguscallback'); - $handler->call(); + $handler = new CallbackHandler('boguscallback'); } public function testCallShouldReturnTheReturnValueOfTheCallback() { - $handler = new CallbackHandler('foo', array('\\ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test')); - if (!is_callable(array('\\ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test'))) { - echo "\nClass exists? " . var_export(class_exists('\\ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback'), 1) . "\n"; + $handler = new CallbackHandler(array('ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test')); + if (!is_callable(array('ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback', 'test'))) { + echo "\nClass exists? " . var_export(class_exists('ZendTest\\Stdlib\\SignalHandlers\\ObjectCallback'), 1) . "\n"; echo "Include path: " . get_include_path() . "\n"; } $this->assertEquals('bar', $handler->call(array())); } - public function testStringCallbackResolvingToClassNameShouldCallViaInvoke() + public function testStringCallbackResolvingToClassDefiningInvokeNameShouldRaiseException() { - $handler = new CallbackHandler('foo', '\\ZendTest\\Stdlib\\SignalHandlers\\Invokable'); - $this->assertEquals('__invoke', $handler->call(), var_export($handler->getCallback(), 1)); + $this->setExpectedException('Zend\Stdlib\Exception\InvalidCallbackException'); + $handler = new CallbackHandler('ZendTest\\Stdlib\\SignalHandlers\\Invokable'); } public function testStringCallbackReferringToClassWithoutDefinedInvokeShouldRaiseException() { $this->setExpectedException('Zend\Stdlib\Exception\InvalidCallbackException'); - $handler = new CallbackHandler('foo', '\\ZendTest\\Stdlib\\SignalHandlers\\InstanceMethod'); + $handler = new CallbackHandler('ZendTest\\Stdlib\\SignalHandlers\\InstanceMethod'); + } + + public function testCallbackConsistingOfStringContextWithNonStaticMethodShouldNotRaiseExceptionButWillRaiseEStrict() + { + $handler = new CallbackHandler(array('ZendTest\\Stdlib\\SignalHandlers\\InstanceMethod', 'handler')); + $error = false; + set_error_handler(function ($errno, $errstr) use (&$error) { + $error = true; + }, E_STRICT); $handler->call(); + restore_error_handler(); + $this->assertTrue($error); } - public function testCallbackConsistingOfStringContextWithNonStaticMethodShouldInstantiateContext() + public function testStringCallbackConsistingOfNonStaticMethodShouldNotRaiseExceptionButWillRaiseEStrict() { - $handler = new CallbackHandler('foo', array( 'ZendTest\\Stdlib\\SignalHandlers\\InstanceMethod', 'callable' )); - $this->assertEquals('callable', $handler->call()); + $handler = new CallbackHandler('ZendTest\\Stdlib\\SignalHandlers\\InstanceMethod::handler'); + $error = false; + set_error_handler(function ($errno, $errstr) use (&$error) { + $error = true; + }, E_STRICT); + $handler->call(); + restore_error_handler(); + $this->assertTrue($error); } - public function testCallbackToClassImplementingOverloadingShouldSucceed() + public function testCallbackToClassImplementingOverloadingButNotInvocableShouldRaiseException() { - $handler = new CallbackHandler('foo', array( '\\ZendTest\\Stdlib\\SignalHandlers\\Overloadable', 'foo' )); - $this->assertEquals('foo', $handler->call()); + $this->setExpectedException('Zend\Stdlib\Exception\InvalidCallbackException'); + $handler = new CallbackHandler('foo', array( 'ZendTest\\Stdlib\\SignalHandlers\\Overloadable', 'foo' )); } public function testClosureCallbackShouldBeInvokedByCall() { - $handler = new CallbackHandler(null, function () { + $handler = new CallbackHandler(function () { return 'foo'; }); $this->assertEquals('foo', $handler->call()); } + public function testHandlerShouldBeInvocable() + { + $handler = new CallbackHandler(array($this, 'handleCall')); + $handler('foo', 'bar'); + $this->assertEquals(array('foo', 'bar'), $this->args); + } + public function handleCall() { $this->args = func_get_args(); diff --git a/test/SignalHandlers/InstanceMethod.php b/test/SignalHandlers/InstanceMethod.php index cf06a79b1..393d32710 100644 --- a/test/SignalHandlers/InstanceMethod.php +++ b/test/SignalHandlers/InstanceMethod.php @@ -3,7 +3,7 @@ class InstanceMethod { - public function callable() + public function handler() { return __FUNCTION__; }