diff --git a/src/CallbackHandler.php b/src/CallbackHandler.php index ae9da5e2a..a42863973 100644 --- a/src/CallbackHandler.php +++ b/src/CallbackHandler.php @@ -23,6 +23,9 @@ */ namespace Zend\Stdlib; +use Closure, + WeakRef; + /** * CallbackHandler * @@ -63,15 +66,65 @@ class CallbackHandler * Constructor * * @param string $event Event to which slot is subscribed - * @param string|array|object $callback PHP callback (first element may be ) + * @param string|array|object $callback PHP callback * @param array $options Options used by the callback handler (e.g., priority) * @return void */ public function __construct($event, $callback, array $options = array()) { $this->event = $event; - $this->callback = $callback; $this->options = $options; + $this->registerCallback($callback); + } + + /** + * Registers the callback provided in the constructor + * + * If you have pecl/weakref {@see http://pecl.php.net/weakref} installed, + * this method provides additional behavior. + * + * 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. + * + * @param callback $callback + * @return void + */ + protected function registerCallback($callback) + { + // If pecl/weakref is not installed, simply register it + if (!class_exists('WeakRef', false)) { + $this->callback = $callback; + return; + } + + // If we have a non-closure object, pass it to WeakRef, and then + // register it. + if (is_object($callback) && !$callback instanceof Closure) { + $this->callback = new WeakRef($callback); + return; + } + + // If we have a string or closure, register as-is + if (!is_array($callback)) { + $this->callback = $callback; + return; + } + + list($target, $method) = $callback; + + // If we have an array callback, and the first argument is not an + // object, register as-is + if (!is_object($target)) { + $this->callback = $callback; + return; + } + + // We have an array callback with an object as the first argument; + // pass it to WeakRef, and then register the new callback + $target = new WeakRef($target); + $this->callback = array($target, $method); } /** @@ -88,26 +141,33 @@ public function getEvent() * Retrieve registered callback * * @return Callback - * @throws Exception\InvalidCallbackException + * @throws Exception\InvalidCallbackException If callback is invalid */ public function getCallback() { - if ($this->isValidCallback) { - return $this->callback; + if (!$this->isValid()) { + throw new Exception\InvalidCallbackException('Invalid callback provided; not callable'); } $callback = $this->callback; if (is_string($callback)) { - return $this->validateStringCallback($callback); + return $callback; } - if (is_array($callback)) { - return $this->validateArrayCallback($callback); + + if ($callback instanceof WeakRef) { + return $callback->get(); } - if (is_callable($callback)) { - $this->isValidCallback = true; + + if (is_object($callback)) { return $callback; } - throw new Exception\InvalidCallbackException('Invalid callback provided; not callable'); + + list($target, $method) = $callback; + if ($target instanceof WeakRef) { + return array($target->get(), $method); + } + + return $callback; } /** @@ -146,6 +206,70 @@ public function getOption($name) 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 + */ + public function isValid() + { + // 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); + } + /** * Validate a string callback * @@ -153,37 +277,35 @@ public function getOption($name) * valid class name; if so, determine if the object is invokable. * * @param string $callback - * @return Callback - * @throws Exception\InvalidCallbackException + * @return bool */ protected function validateStringCallback($callback) { if (is_callable($callback)) { $this->isValidCallback = true; - return $callback; + return true; } if (!class_exists($callback)) { - throw new Exception\InvalidCallbackException('Provided callback is not a function or a class'); + return false; } // check __invoke before instantiating if (!method_exists($callback, '__invoke')) { - throw new Exception\InvalidCallbackException('Class provided as a callback does not implement __invoke'); + return false; } $object = new $callback(); $this->callback = $object; $this->isValidCallback = true; - return $object; + return true; } /** * Validate an array callback * * @param array $callback - * @return callback - * @throws Exception\InvalidCallbackException + * @return bool */ protected function validateArrayCallback(array $callback) { @@ -194,7 +316,7 @@ protected function validateArrayCallback(array $callback) // Dealing with a class/method callback, and class provided is a string classname if (!class_exists($context)) { - throw new Exception\InvalidCallbackException('Class provided in callback does not exist'); + return false; } // We need to determine if we need to instantiate the class first @@ -202,7 +324,7 @@ protected function validateArrayCallback(array $callback) if (!$r->hasMethod($method)) { // Explicit method does not exist if (!$r->hasMethod('__callStatic') && !$r->hasMethod('__call')) { - throw new Exception\InvalidCallbackException('Class provided in callback does not define the method requested'); + return false; } if ($r->hasMethod('__callStatic')) { @@ -238,7 +360,6 @@ protected function validateArrayCallback(array $callback) return $callback; } - - throw new Exception\InvalidCallbackException('Method provided in callback does not exist in object'); + return false; } } diff --git a/src/Dispatchable.php b/src/Dispatchable.php new file mode 100644 index 000000000..39be7623e --- /dev/null +++ b/src/Dispatchable.php @@ -0,0 +1,7 @@ + 0 + ); + } +} diff --git a/src/IteratorToArray.php b/src/IteratorToArray.php new file mode 100644 index 000000000..123bf2e35 --- /dev/null +++ b/src/IteratorToArray.php @@ -0,0 +1,63 @@ +toArray(); + } + + $array = array(); + foreach ($iterator as $key => $value) { + if (is_scalar($value)) { + $array[$key] = $value; + continue; + } + + if ($value instanceof Traversable) { + $array[$key] = static::convert($value, $recursive); + continue; + } + + if (is_array($value)) { + $array[$key] = static::convert($value, $recursive); + continue; + } + + $array[$key] = $value; + } + + return $array; + } +} diff --git a/src/Message.php b/src/Message.php new file mode 100644 index 000000000..71cbc1d39 --- /dev/null +++ b/src/Message.php @@ -0,0 +1,110 @@ +metadata[$spec] = $value; + return $this; + } + if (!is_array($spec) && !$spec instanceof Traversable) { + throw new Exception\InvalidArgumentException(sprintf( + 'Expected a string, array, or Traversable argument in first position; received "%s"', + (is_object($spec) ? get_class($spec) : gettype($spec)) + )); + } + foreach ($spec as $key => $value) { + $this->metadata[$key] = $value; + } + return $this; + } + + /** + * Retrieve all metadata or a single metadatum as specified by key + * + * @param null|string|int $key + * @param null|mixed $default + * @return mixed + */ + public function getMetadata($key = null, $default = null) + { + if (null === $key) { + return $this->metadata; + } + + if (!is_scalar($key)) { + throw new Exception\InvalidArgumentException('Non-scalar argument provided for key'); + } + + if (array_key_exists($key, $this->metadata)) { + return $this->metadata[$key]; + } + + return $default; + } + + /** + * Set message content + * + * @param mixed $value + * @return Message + */ + public function setContent($value) + { + $this->content = $value; + return $this; + } + + /** + * Get message content + * + * @return mixed + */ + public function getContent() + { + return $this->content; + } + + /** + * @return string + */ + public function toString() + { + $request = ''; + foreach ($this->getMetadata() as $key => $value) { + $request .= sprintf( + "%s: %s\r\n", + (string) $key, + (string) $value + ); + } + $request .= "\r\n" . $this->getContent(); + return $request; + } + +} diff --git a/src/MessageDescription.php b/src/MessageDescription.php new file mode 100644 index 000000000..a81ffc98c --- /dev/null +++ b/src/MessageDescription.php @@ -0,0 +1,13 @@ +exchangeArray($values); + } + + /** + * Populate from query string + * + * @param string $string + * @return void + */ + public function fromString($string) + { + $array = array(); + parse_str($string, $array); + $this->fromArray($array); + } + + /** + * Serialize to native PHP array + * + * @return array + */ + public function toArray() + { + return $this->getArrayCopy(); + } + + /** + * Serialize to query string + * + * @return string + */ + public function toString() + { + return http_build_query($this); + } + + /** + * Retrieve by key + * + * Returns null if the key does not exist. + * + * @param string $name + * @return mixed + */ + public function offsetGet($name) + { + if (isset($this[$name])) { + return parent::offsetGet($name); + } + return null; + } + + /** + * @param string $name + * @param mixed $default optional default value + * @return mixed + */ + public function get($name, $default = null) + { + if (isset($this[$name])) { + return parent::offsetGet($name); + } + return $default; + } + + /** + * @param string $name + * @param mixed $value + * @return $this + */ + public function set($name, $value) + { + $this[$name] = $value; + return $this; + } +} diff --git a/src/ParametersDescription.php b/src/ParametersDescription.php new file mode 100644 index 000000000..a52b0c80f --- /dev/null +++ b/src/ParametersDescription.php @@ -0,0 +1,34 @@ + 'bar', + )), + array(array( + 'bar', + 'foo' => 'bar', + 'baz', + )), + ); + } + + public static function invalidAssocArrays() + { + return array( + array(null), + array(true), + array(false), + array(0), + array(1), + array(0.0), + array(1.0), + array('string'), + array(array(0, 1, 2)), + array(new stdClass), + ); + } + + /** + * @dataProvider validAssocArrays + */ + public function testValidAssocArraysReturnTrue($test) + { + $this->assertTrue(IsAssocArray::test($test)); + } + + /** + * @dataProvider invalidAssocArrays + */ + public function testInvalidAssocArraysReturnFalse($test) + { + $this->assertFalse(IsAssocArray::test($test)); + } +} diff --git a/test/IteratorToArrayTest.php b/test/IteratorToArrayTest.php new file mode 100644 index 000000000..5f30b8fb4 --- /dev/null +++ b/test/IteratorToArrayTest.php @@ -0,0 +1,90 @@ + 'bar', + ), array( + 'foo' => 'bar', + )), + array(new Config(array( + 'foo' => array( + 'bar' => array( + 'baz' => array( + 'baz' => 'bat', + ), + ), + ), + )), array( + 'foo' => array( + 'bar' => array( + 'baz' => array( + 'baz' => 'bat', + ), + ), + ), + )), + array(new ArrayObject(array( + 'foo' => array( + 'bar' => array( + 'baz' => array( + 'baz' => 'bat', + ), + ), + ), + )), array( + 'foo' => array( + 'bar' => array( + 'baz' => array( + 'baz' => 'bat', + ), + ), + ), + )), + ); + } + + public static function invalidIterators() + { + return array( + array(null), + array(true), + array(false), + array(0), + array(1), + array(0.0), + array(1.0), + array('string'), + array(new stdClass), + ); + } + + /** + * @dataProvider validIterators + */ + public function testValidIteratorsReturnArrayRepresentation($test, $expected) + { + $result = IteratorToArray::convert($test); + $this->assertEquals($expected, $result); + } + + /** + * @dataProvider invalidIterators + */ + public function testInvalidIteratorsRaiseInvalidArgumentException($test) + { + $this->setExpectedException('Zend\Stdlib\Exception\InvalidArgumentException'); + $this->assertFalse(IteratorToArray::convert($test)); + } +} diff --git a/test/MessageTest.php b/test/MessageTest.php new file mode 100644 index 000000000..bf835a130 --- /dev/null +++ b/test/MessageTest.php @@ -0,0 +1,67 @@ +setContent('I can set content'); + $this->assertInstanceOf('Zend\Stdlib\Message', $ret); + $this->assertEquals('I can set content', $message->getContent()); + } + + public function testMessageCanSetAndGetMetadataKeyAsString() + { + $message = new Message(); + $ret = $message->setMetadata('foo', 'bar'); + $this->assertInstanceOf('Zend\Stdlib\Message', $ret); + $this->assertEquals('bar', $message->getMetadata('foo')); + $this->assertEquals(array('foo' => 'bar'), $message->getMetadata()); + } + + public function testMessageCanSetAndGetMetadataKeyAsArray() + { + $message = new Message(); + $ret = $message->setMetadata(array('foo' => 'bar')); + $this->assertInstanceOf('Zend\Stdlib\Message', $ret); + $this->assertEquals('bar', $message->getMetadata('foo')); + } + + public function testMessageGetMetadataWillUseDefaultValueIfNoneExist() + { + $message = new Message(); + $this->assertEquals('bar', $message->getMetadata('foo', 'bar')); + } + + public function testMessageThrowsExceptionOnInvalidKeyForMetadataSet() + { + $message = new Message(); + + $this->setExpectedException('Zend\Stdlib\Exception\InvalidArgumentException'); + $message->setMetadata(new \stdClass()); + } + + public function testMessageThrowsExceptionOnInvalidKeyForMetadataGet() + { + $message = new Message(); + + $this->setExpectedException('Zend\Stdlib\Exception\InvalidArgumentException'); + $message->getMetadata(new \stdClass()); + } + + public function testMessageToStringWorks() + { + $message = new Message(); + $message->setMetadata(array('Foo' => 'bar', 'One' => 'Two')); + $message->setContent('This is my content'); + $expected = "Foo: bar\r\nOne: Two\r\n\r\nThis is my content"; + $this->assertEquals($expected, $message->toString()); + } +} diff --git a/test/ParametersTest.php b/test/ParametersTest.php new file mode 100644 index 000000000..4d1718cd1 --- /dev/null +++ b/test/ParametersTest.php @@ -0,0 +1,58 @@ +assertInstanceOf('Zend\Stdlib\ParametersDescription', $parameters); + $this->assertInstanceOf('ArrayObject', $parameters); + $this->assertInstanceOf('ArrayAccess', $parameters); + $this->assertInstanceOf('Countable', $parameters); + $this->assertInstanceOf('Serializable', $parameters); + $this->assertInstanceOf('Traversable', $parameters); + } + + public function testParametersPersistNameAndValues() + { + $parameters = new Parameters(array('foo' => 'bar')); + $this->assertEquals('bar', $parameters['foo']); + $this->assertEquals('bar', $parameters->foo); + $parameters->offsetSet('baz', 5); + $this->assertEquals(5, $parameters->baz); + + $parameters->fromArray(array('bar' => 'foo')); + $this->assertEquals('foo', $parameters->bar); + + $parameters->fromString('bar=foo&five=5'); + $this->assertEquals('foo', $parameters->bar); + $this->assertEquals('5', $parameters->five); + $this->assertEquals(array('bar' => 'foo', 'five' => '5'), $parameters->toArray()); + $this->assertEquals('bar=foo&five=5', $parameters->toString()); + + $parameters->fromArray(array()); + $parameters->set('foof', 'barf'); + $this->assertEquals('barf', $parameters->get('foof')); + $this->assertEquals('barf', $parameters->foof); + + } + + public function testParametersOffsetgetReturnsNullIfNonexistentKeyIsProvided() + { + $parameters = new Parameters; + $this->assertNull($parameters->foo); + } + + public function testParametersGetReturnsDefaultValueIfNonExistent() + { + $parameters = new Parameters(); + + $this->assertEquals(5, $parameters->get('nonExistentProp', 5)); + } + +}