Permalink
Browse files

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
  • Loading branch information...
malkusch committed Dec 24, 2015
1 parent b4c584f commit c9cd844fcf4a364d021817acaea5fc031abf7e86
Showing with 268 additions and 1 deletion.
  1. +7 −0 README.md
  2. +4 −1 composer.json
  3. +123 −0 overwrites/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php
  4. +134 −0 tests/SandboxTest.php
View
@@ -49,6 +49,13 @@ This library comes with the same restrictions as the underlying
this issue you can call [`PHPProphet::define()`](http://php-mock.github.io/php-mock-prophecy/api/class-phpmock.prophecy.PHPProphet.html#_define)
before that first call. This would define a side effectless namespaced function.
* Additionally it shares restrictions from Prophecy as well:
Prophecy [doesn't support pass-by-reference](https://github.com/phpspec/prophecy/issues/225).
To support pass-by-reference here, an implicit feature of composer
is used. It may work accidentially but can change at any moment.
If you need pass-by-reference in prophecies, consider using another framework
(e.g. [php-mock-phpunit](https://github.com/php-mock/php-mock-phpunit)).
# License and authors
This project is free and under the WTFPL.
View
@@ -14,7 +14,10 @@
}
],
"autoload": {
"psr-4": {"phpmock\\prophecy\\": "classes/"}
"psr-4": {
"phpmock\\prophecy\\": "classes/",
"Prophecy\\": "overwrites/Prophecy/"
}
},
"require": {
"php": ">=5.5",
@@ -0,0 +1,123 @@
<?php
/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
* Marcello Duarte <marcello.duarte@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Prophecy\Doubler\ClassPatch;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\ArgumentNode;
/**
* Add Prophecy functionality to the double.
* This is a core class patch for Prophecy.
*
* @author Konstantin Kudryashov <ever.zet@gmail.com>
*/
class ProphecySubjectPatch implements ClassPatchInterface
{
/**
* Always returns true.
*
* @param ClassNode $node
*
* @return bool
*/
public function supports(ClassNode $node)
{
return true;
}
/**
* Apply Prophecy functionality to class node.
*
* @param ClassNode $node
*/
public function apply(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
$node->addProperty('objectProphecy', 'private');
foreach ($node->getMethods() as $name => $method) {
if ('__construct' === strtolower($name)) {
continue;
}
$method->setCode(
sprintf(
'$arguments = %s;'
.'$variadics = array_slice(func_get_args(), count($arguments));'
.'$arguments = array_merge($arguments, $variadics);'
.'return $this->getProphecy()->makeProphecyMethodCall(__FUNCTION__, $arguments);',
$this->buildBodyArgumentsArray($method)
)
);
}
$prophecySetter = new MethodNode('setProphecy');
$prophecyArgument = new ArgumentNode('prophecy');
$prophecyArgument->setTypeHint('Prophecy\Prophecy\ProphecyInterface');
$prophecySetter->addArgument($prophecyArgument);
$prophecySetter->setCode('$this->objectProphecy = $prophecy;');
$prophecyGetter = new MethodNode('getProphecy');
$prophecyGetter->setCode('return $this->objectProphecy;');
if ($node->hasMethod('__call')) {
$__call = $node->getMethod('__call');
} else {
$__call = new MethodNode('__call');
$__call->addArgument(new ArgumentNode('name'));
$__call->addArgument(new ArgumentNode('arguments'));
$node->addMethod($__call);
}
$__call->setCode(<<<PHP
throw new \Prophecy\Exception\Doubler\MethodNotFoundException(
sprintf('Method `%s::%s()` not found.', get_class(\$this), func_get_arg(0)),
\$this->getProphecy(), func_get_arg(0)
);
PHP
);
$node->addMethod($prophecySetter);
$node->addMethod($prophecyGetter);
}
private function buildBodyArgumentsArray(MethodNode $method)
{
// TODO support variadics as well. See #91.
$arguments = [];
foreach ($method->getArguments() as $argument) {
if ($argument->isOptional()) {
continue;
}
if ($argument->isPassedByReference()) {
$arguments[] = sprintf("&$%s", $argument->getName());
} else {
$arguments[] = sprintf("$%s", $argument->getName());
}
}
$php = implode(", ", $arguments);
return "[$php]";
}
/**
* Returns patch priority, which determines when patch will be applied.
*
* @return int Priority number (higher - earlier)
*/
public function getPriority()
{
return 0;
}
}
View
@@ -0,0 +1,134 @@
<?php
namespace test;
use phpmock\prophecy\PHPProphet;
class Foo
{
public function noArgument()
{
throw new \RuntimeException("not mocked");
}
public function argument($arg)
{
throw new \RuntimeException("not mocked");
}
public function variadics1($arg1, $arg2 = 'default')
{
throw new \RuntimeException("not mocked");
}
// not supported https://github.com/phpspec/prophecy/issues/91
/*public function variadics2(...$args)
{
throw new \RuntimeException("not mocked");
}
*/
public function reference(&$arg)
{
throw new \RuntimeException("not mocked");
}
}
class SandboxTest extends \PHPUnit_Framework_TestCase
{
/**
* @var \Prophecy\Prophet()
*/
private $prophet;
/**
* @var type
*/
private $prophecy;
protected function setUp()
{
parent::setUp();
$revealer = new \phpmock\prophecy\ReferencePreservingRevealer(new \Prophecy\Prophecy\Revealer());
$this->prophet = new \Prophecy\Prophet(null, $revealer);
$this->prophecy = $this->prophet->prophesize(Foo::class);
}
public function testPHPProphet()
{
$prophet = new PHPProphet();
$prophecy = $prophet->prophesize(__NAMESPACE__);
$prophecy->time()->willReturn(123);
$prophecy->reveal();
assert(123 == time());
$prophet->checkPredictions();
}
public function testNoArgument()
{
$this->prophecy->noArgument()->willReturn("noarument");
$foo = $this->prophecy->reveal();
$this->assertEquals("noarument", $foo->noArgument());
}
public function testOneArgument()
{
$this->prophecy->argument("foo")->willReturn("bar");
$foo = $this->prophecy->reveal();
$this->assertEquals("bar", $foo->argument("foo"));
}
public function testVariadics1Argument()
{
$this->prophecy->variadics1("foo1")->willReturn("bar1");
$this->prophecy->variadics1("foo1", "bar1")->willReturn("bar2");
$this->prophecy->variadics1("foo2")->willReturnArgument();
$this->prophecy->variadics1(1, 2)->will(function ($args) {
return array_sum($args);
});
$foo = $this->prophecy->reveal();
$this->assertEquals("bar1", $foo->variadics1("foo1"));
$this->assertEquals("bar2", $foo->variadics1("foo1", "bar1"));
$this->assertEquals("foo2", $foo->variadics1("foo2"));
$this->assertEquals(3, $foo->variadics1(1, 2));
}
public function testReference()
{
$this->prophecy->reference(\Prophecy\Argument::cetera())->will(function (array $args) {
call_user_func_array(function (&$arg) {
$arg = "test4";
}, $args);
});
$foo = $this->prophecy->reveal();
$ref="ref";
$foo->reference($ref);
$this->assertEquals("test4", $ref);
}
public function testReference2()
{
$this->prophecy->reference(\Prophecy\Argument::cetera())->will(function (array $args) {
$args[0] = "test5";
});
$foo = $this->prophecy->reveal();
$ref="ref";
$foo->reference($ref);
$this->assertEquals("test5", $ref);
}
}

0 comments on commit c9cd844

Please sign in to comment.