Skip to content

Commit c9cd844

Browse files
committed
Support Pass-By-Reference
To support prophecies for e.g. exec(), pass-by-reference is required. Prophecy doesn't support pass-by-reference. This just happens because ProphecySubjectPatch::apply() swallows them with an offending func_get_args(). Upstream seems to be reluctant to change that behaviour as they don't want to support pass-by-reference at all. Therefore I have to overwrite the class ProphecySubjectPatch completely and patch the desired behaviour in. Overwriting classes is AFAIK a non intended behaviour of composer. It may work accidentially but can change at any moment. I'll check with composer if this could become an explicit feature. But for now support for pass-by-reference is unreliable. If you need pass-by-reference in prophecies, consider using another framework (e.g. https://github.com/php-mock/php-mock-phpunit). See also: phpspec/prophecy#225
1 parent b4c584f commit c9cd844

File tree

4 files changed

+268
-1
lines changed

4 files changed

+268
-1
lines changed

README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,13 @@ This library comes with the same restrictions as the underlying
4949
this issue you can call [`PHPProphet::define()`](http://php-mock.github.io/php-mock-prophecy/api/class-phpmock.prophecy.PHPProphet.html#_define)
5050
before that first call. This would define a side effectless namespaced function.
5151

52+
* Additionally it shares restrictions from Prophecy as well:
53+
Prophecy [doesn't support pass-by-reference](https://github.com/phpspec/prophecy/issues/225).
54+
To support pass-by-reference here, an implicit feature of composer
55+
is used. It may work accidentially but can change at any moment.
56+
If you need pass-by-reference in prophecies, consider using another framework
57+
(e.g. [php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit)).
58+
5259
# License and authors
5360

5461
This project is free and under the WTFPL.

composer.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,10 @@
1414
}
1515
],
1616
"autoload": {
17-
"psr-4": {"phpmock\\prophecy\\": "classes/"}
17+
"psr-4": {
18+
"phpmock\\prophecy\\": "classes/",
19+
"Prophecy\\": "overwrites/Prophecy/"
20+
}
1821
},
1922
"require": {
2023
"php": ">=5.5",
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Prophecy.
5+
* (c) Konstantin Kudryashov <[email protected]>
6+
* Marcello Duarte <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Prophecy\Doubler\ClassPatch;
13+
14+
use Prophecy\Doubler\Generator\Node\ClassNode;
15+
use Prophecy\Doubler\Generator\Node\MethodNode;
16+
use Prophecy\Doubler\Generator\Node\ArgumentNode;
17+
18+
/**
19+
* Add Prophecy functionality to the double.
20+
* This is a core class patch for Prophecy.
21+
*
22+
* @author Konstantin Kudryashov <[email protected]>
23+
*/
24+
class ProphecySubjectPatch implements ClassPatchInterface
25+
{
26+
/**
27+
* Always returns true.
28+
*
29+
* @param ClassNode $node
30+
*
31+
* @return bool
32+
*/
33+
public function supports(ClassNode $node)
34+
{
35+
return true;
36+
}
37+
38+
/**
39+
* Apply Prophecy functionality to class node.
40+
*
41+
* @param ClassNode $node
42+
*/
43+
public function apply(ClassNode $node)
44+
{
45+
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
46+
$node->addProperty('objectProphecy', 'private');
47+
48+
foreach ($node->getMethods() as $name => $method) {
49+
if ('__construct' === strtolower($name)) {
50+
continue;
51+
}
52+
53+
$method->setCode(
54+
sprintf(
55+
'$arguments = %s;'
56+
.'$variadics = array_slice(func_get_args(), count($arguments));'
57+
.'$arguments = array_merge($arguments, $variadics);'
58+
.'return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, $arguments);',
59+
$this->buildBodyArgumentsArray($method)
60+
)
61+
);
62+
}
63+
64+
$prophecySetter = new MethodNode('setProphecy');
65+
$prophecyArgument = new ArgumentNode('prophecy');
66+
$prophecyArgument->setTypeHint('Prophecy\Prophecy\ProphecyInterface');
67+
$prophecySetter->addArgument($prophecyArgument);
68+
$prophecySetter->setCode('$this->objectProphecy = $prophecy;');
69+
70+
$prophecyGetter = new MethodNode('getProphecy');
71+
$prophecyGetter->setCode('return $this->objectProphecy;');
72+
73+
if ($node->hasMethod('__call')) {
74+
$__call = $node->getMethod('__call');
75+
} else {
76+
$__call = new MethodNode('__call');
77+
$__call->addArgument(new ArgumentNode('name'));
78+
$__call->addArgument(new ArgumentNode('arguments'));
79+
80+
$node->addMethod($__call);
81+
}
82+
83+
$__call->setCode(<<<PHP
84+
throw new \Prophecy\Exception\Doubler\MethodNotFoundException(
85+
sprintf('Method `%s::%s()` not found.', get_class(\$this), func_get_arg(0)),
86+
\$this->getProphecy(), func_get_arg(0)
87+
);
88+
PHP
89+
);
90+
91+
$node->addMethod($prophecySetter);
92+
$node->addMethod($prophecyGetter);
93+
}
94+
95+
private function buildBodyArgumentsArray(MethodNode $method)
96+
{
97+
// TODO support variadics as well. See #91.
98+
$arguments = [];
99+
foreach ($method->getArguments() as $argument) {
100+
if ($argument->isOptional()) {
101+
continue;
102+
}
103+
104+
if ($argument->isPassedByReference()) {
105+
$arguments[] = sprintf("&$%s", $argument->getName());
106+
} else {
107+
$arguments[] = sprintf("$%s", $argument->getName());
108+
}
109+
}
110+
$php = implode(", ", $arguments);
111+
return "[$php]";
112+
}
113+
114+
/**
115+
* Returns patch priority, which determines when patch will be applied.
116+
*
117+
* @return int Priority number (higher - earlier)
118+
*/
119+
public function getPriority()
120+
{
121+
return 0;
122+
}
123+
}

tests/SandboxTest.php

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
<?php
2+
3+
namespace test;
4+
5+
use phpmock\prophecy\PHPProphet;
6+
7+
class Foo
8+
{
9+
10+
public function noArgument()
11+
{
12+
throw new \RuntimeException("not mocked");
13+
}
14+
15+
public function argument($arg)
16+
{
17+
throw new \RuntimeException("not mocked");
18+
}
19+
20+
public function variadics1($arg1, $arg2 = 'default')
21+
{
22+
throw new \RuntimeException("not mocked");
23+
}
24+
25+
// not supported https://github.com/phpspec/prophecy/issues/91
26+
/*public function variadics2(...$args)
27+
{
28+
throw new \RuntimeException("not mocked");
29+
}
30+
*/
31+
32+
public function reference(&$arg)
33+
{
34+
throw new \RuntimeException("not mocked");
35+
}
36+
}
37+
38+
class SandboxTest extends \PHPUnit_Framework_TestCase
39+
{
40+
41+
/**
42+
* @var \Prophecy\Prophet()
43+
*/
44+
private $prophet;
45+
46+
/**
47+
* @var type
48+
*/
49+
private $prophecy;
50+
51+
protected function setUp()
52+
{
53+
parent::setUp();
54+
55+
$revealer = new \phpmock\prophecy\ReferencePreservingRevealer(new \Prophecy\Prophecy\Revealer());
56+
57+
$this->prophet = new \Prophecy\Prophet(null, $revealer);
58+
$this->prophecy = $this->prophet->prophesize(Foo::class);
59+
}
60+
61+
public function testPHPProphet()
62+
{
63+
$prophet = new PHPProphet();
64+
65+
$prophecy = $prophet->prophesize(__NAMESPACE__);
66+
$prophecy->time()->willReturn(123);
67+
$prophecy->reveal();
68+
69+
assert(123 == time());
70+
$prophet->checkPredictions();
71+
}
72+
73+
public function testNoArgument()
74+
{
75+
$this->prophecy->noArgument()->willReturn("noarument");
76+
77+
$foo = $this->prophecy->reveal();
78+
$this->assertEquals("noarument", $foo->noArgument());
79+
}
80+
81+
public function testOneArgument()
82+
{
83+
$this->prophecy->argument("foo")->willReturn("bar");
84+
85+
$foo = $this->prophecy->reveal();
86+
$this->assertEquals("bar", $foo->argument("foo"));
87+
}
88+
89+
public function testVariadics1Argument()
90+
{
91+
$this->prophecy->variadics1("foo1")->willReturn("bar1");
92+
$this->prophecy->variadics1("foo1", "bar1")->willReturn("bar2");
93+
94+
$this->prophecy->variadics1("foo2")->willReturnArgument();
95+
$this->prophecy->variadics1(1, 2)->will(function ($args) {
96+
return array_sum($args);
97+
});
98+
99+
$foo = $this->prophecy->reveal();
100+
$this->assertEquals("bar1", $foo->variadics1("foo1"));
101+
$this->assertEquals("bar2", $foo->variadics1("foo1", "bar1"));
102+
$this->assertEquals("foo2", $foo->variadics1("foo2"));
103+
$this->assertEquals(3, $foo->variadics1(1, 2));
104+
}
105+
106+
public function testReference()
107+
{
108+
$this->prophecy->reference(\Prophecy\Argument::cetera())->will(function (array $args) {
109+
call_user_func_array(function (&$arg) {
110+
$arg = "test4";
111+
112+
}, $args);
113+
});
114+
115+
$foo = $this->prophecy->reveal();
116+
117+
$ref="ref";
118+
$foo->reference($ref);
119+
$this->assertEquals("test4", $ref);
120+
}
121+
122+
public function testReference2()
123+
{
124+
$this->prophecy->reference(\Prophecy\Argument::cetera())->will(function (array $args) {
125+
$args[0] = "test5";
126+
});
127+
128+
$foo = $this->prophecy->reveal();
129+
130+
$ref="ref";
131+
$foo->reference($ref);
132+
$this->assertEquals("test5", $ref);
133+
}
134+
}

0 commit comments

Comments
 (0)