diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9b2f932bd..cdd5cc0dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,13 +2,14 @@
## Next release
-- **[NEW]** Implemented generator stubs ([#11]).
+- **[NEW]** Implemented generator stubs ([#11], [#140]).
- **[IMPROVED]** More default values for built-in return types ([#138], [#139]).
- **[FIXED]** Fixed memory leak under PHP 7 ([#143]).
[#11]: https://github.com/eloquent/phony/issues/11
[#138]: https://github.com/eloquent/phony/issues/138
[#139]: https://github.com/eloquent/phony/pull/139
+[#140]: https://github.com/eloquent/phony/issues/140
[#143]: https://github.com/eloquent/phony/issues/143
## 0.8.0 (2016-02-12)
diff --git a/doc/index.md b/doc/index.md
index e50a769a5..a537309a4 100644
--- a/doc/index.md
+++ b/doc/index.md
@@ -65,6 +65,8 @@
- [Invoking callables]
- [Stubbing generators]
- [Yielding from a generator]
+ - [Yielding individual values from a generator]
+ - [Yielding multiple values from a generator]
- [Returning values from a generator]
- [Returning arguments from a generator]
- [Returning the "self" value from a generator]
@@ -2566,7 +2568,7 @@ Add a yielded value to the answer.
*If no arguments are supplied, the stub will yield like `yield;`.*
-*See [Yielding from a generator].*
+*See [Yielding individual values from a generator].*
@@ -2579,13 +2581,13 @@ Add a set of yielded values to the answer.
*The `$values` argument can be an array, an iterator, or even another
generator.*
-*See [Yielding from a generator].*
+*See [Yielding multiple values from a generator].*
----
-> *[stub][stub-api]* $generatorAnswer->[**returns**](#generatorAnswer.returns)($value = null)
+> *[stub][stub-api]* $generatorAnswer->[**returns**](#generatorAnswer.returns)($value = null, ...$additionalValues)
End the generator by returning a value.
@@ -2620,7 +2622,7 @@ End the generator by returning the self value.
----
-> *[stub][stub-api]* $generatorAnswer->[**throws**](#generatorAnswer.throws)($exception = null)
+> *[stub][stub-api]* $generatorAnswer->[**throws**](#generatorAnswer.throws)($exception = null, ...$additionalExceptions)
End the generator by throwing an exception.
@@ -3447,11 +3449,12 @@ echo json_encode($values); // outputs '[]'
```
The result of [`generates()`](#stub.generates) is a [generator answer]. This
-object can be used to further customize the behavior of the generator to be
-returned. See the subsequent headings for details of these customizations.
+object can be used to further customize the behavior of the generator. See the
+subsequent headings for details of these customizations.
-When a method is called on the generator answer that "ends" the answer (by
-returning or throwing), the original stub is returned, allowing continued
+Certain methods, such as [`returns()`](#generatorAnswer.returns), or
+[`throws()`](#generatorAnswer.throws), mark the "end" of generator answer. When
+a generator answer is "ended", the original stub is returned, allowing continued
stubbing in a fluent manner:
```php
@@ -3478,7 +3481,7 @@ echo $resultC instanceof Generator ? 'true' : 'false'; // outputs 'true'
#### Yielding from a generator
Keys and values to be yielded can be passed directly to
-[`generates()`](#stub.generates) as an array:
+[`generates()`](#stub.generates) as any traversable value:
```php
$stub = stub()
@@ -3495,8 +3498,11 @@ echo json_encode($valuesA); // outputs '["a","b","c","d"]'
echo json_encode($valuesB); // outputs '{"e":"f","g":"h"}'
```
-Alternatively, [`yields()`](#generatorAnswer.yields) can be used when yields
-need to be interleaved with other actions:
+##### Yielding individual values from a generator
+
+For more complicated generator behavior stubbing,
+[`yields()`](#generatorAnswer.yields) can be used to interleave yields with
+other actions:
```php
$count = 0;
@@ -3542,6 +3548,8 @@ $values = iterator_to_array($stub()); // consume the generator
echo json_encode($values); // outputs '{"a":"b","0":"c","1":null}'
```
+##### Yielding multiple values from a generator
+
To yield a set of values from an array, an iterator, or another generator, use
[`yieldsFrom()`](#generatorAnswer.yieldsFrom):
@@ -3595,6 +3603,35 @@ iterator_to_array($generator); // consume the generator
echo $generator->getReturn(); // outputs 'a'
```
+Calling [`returns()`](#generatorAnswer.returns) with multiple arguments allows
+for easy specification of the generator return value on subsequent invocations.
+For example, the two following stubs behave the same:
+
+```php
+$stubA = stub()
+ ->generates()->returns('x', 'y');
+
+$generatorA = $stubA();
+$generatorB = $stubA();
+iterator_to_array($generatorA);
+iterator_to_array($generatorB);
+
+echo $generatorA->getReturn(); // outputs 'x'
+echo $generatorB->getReturn(); // outputs 'y'
+
+$stubB = stub()
+ ->generates()->returns('x')
+ ->generates()->returns('y');
+
+$generatorA = $stubB();
+$generatorB = $stubB();
+iterator_to_array($generatorA);
+iterator_to_array($generatorB);
+
+echo $generatorA->getReturn(); // outputs 'x'
+echo $generatorB->getReturn(); // outputs 'y'
+```
+
Note that attempting to return anything other than `null` will result in an
exception unless the current runtime supports generator return expressions. For
older runtimes, it is perfectly valid to call
@@ -3631,7 +3668,7 @@ echo $generatorC->getReturn(); // outputs 'z'
#### Returning the "self" value from a generator
-The stub [self value] can be return from a generator by using
+The stub [self value] can be returned from a generator by using
[`returnsSelf()`](#generatorAnswer.returnsSelf) on any [generator answer]:
```php
@@ -3664,6 +3701,34 @@ iterator_to_array($generatorA); // throws $exception
iterator_to_array($generatorB); // throws a generic exception
```
+Calling [`throws()`](#generatorAnswer.throws) with multiple arguments allows for
+easy specification of the thrown exception on subsequent invocations. For
+example, the two following stubs behave the same:
+
+```php
+$exceptionA = new RuntimeException('You done goofed.');
+$exceptionB = new RuntimeException('Consequences will never be the same.');
+
+$stubA = stub()
+ ->generates()->throws($exceptionA, $exceptionB);
+
+$generatorA = $stubA();
+$generatorB = $stubA();
+
+iterator_to_array($generatorA); // throws $exceptionA
+iterator_to_array($generatorB); // throws $exceptionB
+
+$stubB = stub()
+ ->generates()->throws($exceptionA)
+ ->generates()->throws($exceptionB);
+
+$generatorA = $stubB();
+$generatorB = $stubB();
+
+iterator_to_array($generatorA); // throws $exceptionA
+iterator_to_array($generatorB); // throws $exceptionB
+```
+
#### Generator iterations that perform multiple actions
Stubbed generators can perform multiple actions as part of a single iteration.
@@ -7291,6 +7356,8 @@ For the full copyright and license information, please view the [LICENSE file].
[verifying values received by spies]: #verifying-values-received-by-spies
[when to use the "equal to" matcher]: #when-to-use-the-equal-to-matcher
[yielding from a generator]: #yielding-from-a-generator
+[yielding individual values from a generator]: #yielding-individual-values-from-a-generator
+[yielding multiple values from a generator]: #yielding-multiple-values-from-a-generator
diff --git a/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php b/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php
index 6dc335e14..1ccb16db3 100644
--- a/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php
+++ b/src/Stub/Answer/Builder/GeneratorAnswerBuilder.php
@@ -337,12 +337,20 @@ public function yieldsFrom($values)
* End the generator by returning a value.
*
* @param mixed $value The return value.
+ * @param mixed ...$additionalValues Additional return values for subsequent invocations.
*
* @return StubInterface The stub.
* @throws RuntimeException If the current runtime does not support the supplied return value.
*/
public function returns($value = null)
{
+ $argumentCount = func_num_args();
+ $copies = array();
+
+ for ($i = 1; $i < $argumentCount; ++$i) {
+ $copies[$i] = clone $this;
+ }
+
if (
$value instanceof InstanceHandleInterface &&
$value->isAdaptable()
@@ -363,6 +371,13 @@ public function returns($value = null)
}
// @codeCoverageIgnoreEnd
+ for ($i = 1; $i < $argumentCount; ++$i) {
+ $this->stub
+ ->doesWith($copies[$i]->answer(), array(), true, true, false);
+
+ $copies[$i]->returns(func_get_arg($i));
+ }
+
return $this->stub;
}
@@ -415,11 +430,19 @@ public function returnsSelf()
* End the generator by throwing an exception.
*
* @param Exception|Error|string|null $exception The exception, or message, or null to throw a generic exception.
+ * @param Exception|Error|string ...$additionalExceptions Additional exceptions, or messages, for subsequent invocations.
*
* @return StubInterface The stub.
*/
public function throws($exception = null)
{
+ $argumentCount = func_num_args();
+ $copies = array();
+
+ for ($i = 1; $i < $argumentCount; ++$i) {
+ $copies[$i] = clone $this;
+ }
+
if (is_string($exception)) {
$exception = new Exception($exception);
} elseif (
@@ -433,6 +456,13 @@ public function throws($exception = null)
$this->exception = $exception;
+ for ($i = 1; $i < $argumentCount; ++$i) {
+ $this->stub
+ ->doesWith($copies[$i]->answer(), array(), true, true, false);
+
+ $copies[$i]->throws(func_get_arg($i));
+ }
+
return $this->stub;
}
@@ -468,6 +498,18 @@ public function answer()
// @codeCoverageIgnoreEnd
}
+ /**
+ * Clone this builder.
+ */
+ public function __clone()
+ {
+ // explicitly break references
+ foreach (get_object_vars($this) as $property => $value) {
+ unset($this->$property);
+ $this->$property = $value;
+ }
+ }
+
private $stub;
private $isGeneratorReturnSupported;
private $invocableInspector;
diff --git a/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php b/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php
index 6ce9d6796..e1e2b5d9c 100644
--- a/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php
+++ b/src/Stub/Answer/Builder/GeneratorAnswerBuilderInterface.php
@@ -146,6 +146,7 @@ public function yieldsFrom($values);
* @api
*
* @param mixed $value The return value.
+ * @param mixed ...$additionalValues Additional return values for subsequent invocations.
*
* @return StubInterface The stub.
* @throws RuntimeException If the current runtime does not support the supplied return value.
@@ -181,6 +182,7 @@ public function returnsSelf();
* @api
*
* @param Exception|Error|string|null $exception The exception, or message, or null to throw a generic exception.
+ * @param Exception|Error|string ...$additionalExceptions Additional exceptions, or messages, for subsequent invocations.
*
* @return StubInterface The stub.
*/
diff --git a/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php b/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php
index 00732dbde..6088cafd4 100644
--- a/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php
+++ b/test/suite-generators/Stub/Answer/Builder/GeneratorAnswerBuilderTest.php
@@ -373,6 +373,20 @@ public function testReturnsWithValue()
$this->markTestSkipped('Requires generator return values.');
}
+ $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c'));
+
+ $generator = call_user_func($this->answer, $this->self, $this->arguments);
+
+ $this->assertSame(array('a', 'b'), iterator_to_array($generator));
+ $this->assertSame('c', $generator->getReturn());
+ }
+
+ public function testReturnsWithInstanceHandleValue()
+ {
+ if (!$this->featureDetector->isSupported('generator.return')) {
+ $this->markTestSkipped('Requires generator return values.');
+ }
+
$adaptable = Phony::mock();
$this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns($adaptable));
@@ -382,18 +396,25 @@ public function testReturnsWithValue()
$this->assertSame($adaptable->mock(), $generator->getReturn());
}
- public function testReturnsWithInstanceHandleValue()
+ public function testReturnsWithMulitpleValues()
{
if (!$this->featureDetector->isSupported('generator.return')) {
$this->markTestSkipped('Requires generator return values.');
}
- $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c'));
+ $this->stub->doesWith($this->answer, array(), true, true, false);
- $generator = call_user_func($this->answer, $this->self, $this->arguments);
+ $this->assertSame($this->stub, $this->subject->yields('a')->yields('b')->returns('c', 'd'));
+
+ $generator = call_user_func($this->stub);
$this->assertSame(array('a', 'b'), iterator_to_array($generator));
$this->assertSame('c', $generator->getReturn());
+
+ $generator = call_user_func($this->stub);
+
+ $this->assertSame(array('a', 'b'), iterator_to_array($generator));
+ $this->assertSame('d', $generator->getReturn());
}
public function testReturnsFailureValueNotSupported()
@@ -563,6 +584,43 @@ public function testThrowsWithException()
$this->assertSame($exception, $actual);
}
+ public function testThrowsWithMultipleExceptions()
+ {
+ $this->stub->doesWith($this->answer, array(), true, true, false);
+ $exceptionA = new Exception('a');
+ $exceptionB = new Exception('b');
+ $this->subject->throws($exceptionA, $exceptionB);
+ $generator = call_user_func($this->stub);
+ $actual = null;
+
+ try {
+ iterator_to_array($generator);
+ } catch (Exception $actual) {
+ }
+
+ $this->assertSame($exceptionA, $actual);
+
+ $generator = call_user_func($this->stub);
+ $actual = null;
+
+ try {
+ iterator_to_array($generator);
+ } catch (Exception $actual) {
+ }
+
+ $this->assertSame($exceptionB, $actual);
+
+ $generator = call_user_func($this->stub);
+ $actual = null;
+
+ try {
+ iterator_to_array($generator);
+ } catch (Exception $actual) {
+ }
+
+ $this->assertSame($exceptionB, $actual);
+ }
+
public function testThrowsWithMessage()
{
$exception = new Exception();
diff --git a/test/suite-generators/Stub/StubWithGeneratorsTest.php b/test/suite-generators/Stub/StubWithGeneratorsTest.php
index de575b5a2..a9682d84b 100644
--- a/test/suite-generators/Stub/StubWithGeneratorsTest.php
+++ b/test/suite-generators/Stub/StubWithGeneratorsTest.php
@@ -150,6 +150,13 @@ public function testGenerates()
$this->assertInstanceOf('Generator', $generator);
$this->assertSame($this->subject, $builder->returns());
$this->assertSame(array('a' => 'b', 0 => 'c'), $actual);
+
+ $generator = call_user_func($this->subject);
+ $actual = iterator_to_array($generator);
+
+ $this->assertInstanceOf('Generator', $generator);
+ $this->assertSame($this->subject, $builder->returns());
+ $this->assertSame(array('a' => 'b', 0 => 'c'), $actual);
}
public function testGeneratesWithMultipleArguments()
@@ -157,9 +164,11 @@ public function testGeneratesWithMultipleArguments()
$builder = $this->subject->generates(array('a'), array('b'));
$actualA = iterator_to_array(call_user_func($this->subject));
$actualB = iterator_to_array(call_user_func($this->subject));
+ $actualC = iterator_to_array(call_user_func($this->subject));
$this->assertInstanceOf('Eloquent\Phony\Stub\Answer\Builder\GeneratorAnswerBuilderInterface', $builder);
$this->assertSame(array('a'), $actualA);
$this->assertSame(array('b'), $actualB);
+ $this->assertSame(array('b'), $actualC);
}
}