Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Readonly support #623

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 8 additions & 2 deletions spec/Prophecy/Argument/ArgumentsWildcardSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ function it_wraps_non_token_arguments_into_ExactValueToken(\stdClass $object)
$class = get_class($object->getWrappedObject());
$id = spl_object_id($object->getWrappedObject());

$objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
$objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n" .
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
" 'closure' => Closure#%s Object (\n" .
" 0 => Closure#%s Object\n" .
" )\n" .
" )\n" .
"))";

$idRegexExpr = '[0-9]+';
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr)));
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr, $idRegexExpr)));
}

function it_generates_string_representation_from_all_tokens_imploded(
Expand Down
10 changes: 8 additions & 2 deletions spec/Prophecy/Argument/Token/ExactValueTokenSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,18 @@ function it_generates_proper_string_representation_for_object(\stdClass $object)
$objHash = sprintf('exact(%s#%s',
get_class($object->getWrappedObject()),
spl_object_id($object->getWrappedObject())
) . " Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
) . " Object (\n" .
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
" 'closure' => Closure#%s Object (\n" .
" 0 => Closure#%s Object\n" .
" )\n" .
" )\n" .
"))";

$this->beConstructedWith($object);

$idRegexExpr = '[0-9]+';
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr)));
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr, $idRegexExpr)));
}

function it_scores_10_if_value_an_numeric_and_equal_to_argument_as_stringable()
Expand Down
10 changes: 8 additions & 2 deletions spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -144,11 +144,17 @@ function it_generates_proper_string_representation_for_object($object)
$objHash = sprintf('identical(%s#%s',
get_class($object->getWrappedObject()),
spl_object_id($object->getWrappedObject())
) . " Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
) . " Object (\n" .
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
" 'closure' => Closure#%s Object (\n" .
" 0 => Closure#%s Object\n" .
" )\n" .
" )\n" .
"))";

$this->beConstructedWith($object);

$idRegexExpr = '[0-9]+';
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr)));
$this->__toString()->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr, $idRegexExpr)));
}
}
14 changes: 12 additions & 2 deletions spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Prophecy\Argument;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\PropertyTypeNode;
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;

class ProphecySubjectPatchSpec extends ObjectBehavior
Expand All @@ -28,8 +29,12 @@ function it_supports_any_class(ClassNode $node)
function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->shouldBeCalled();
$node->addProperty(
'objectProphecyClosureContainer',
'private',
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
)->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));

$node->addProperty('objectProphecyClosure', 'private')->willReturn(null);
$node->getMethods()->willReturn(array());
$node->hasMethod(Argument::any())->willReturn(false);
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);
Expand All @@ -47,7 +52,12 @@ function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prop
MethodNode $method4
) {
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->willReturn(null);
$node->addProperty('objectProphecyClosure', 'private')->willReturn(null);
$node->addProperty(
'objectProphecyClosureContainer',
'private',
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
)->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));

$node->hasMethod(Argument::any())->willReturn(false);
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);
Expand Down
21 changes: 13 additions & 8 deletions spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use Prophecy\Doubler\Generator\Node\ArgumentTypeNode;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\PropertyNode;
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;

class ClassCodeGeneratorSpec extends ObjectBehavior
Expand All @@ -30,7 +31,11 @@ function it_generates_proper_php_code_for_specific_ClassNode(
$class->getInterfaces()->willReturn(array(
'Prophecy\Doubler\Generator\MirroredInterface', 'ArrayAccess', 'ArrayIterator'
));
$class->getProperties()->willReturn(array('name' => 'public', 'email' => 'private'));
$name = new PropertyNode('name');
$name->setVisibility('public');
$email = new PropertyNode('email');
$email->setVisibility('private');
$class->getPropertyNodes()->willReturn(array('name' => $name, 'email' => $email));
$class->getMethods()->willReturn(array($method1, $method2, $method3, $method4, $method5));
$class->isReadOnly()->willReturn(false);

Expand Down Expand Up @@ -153,7 +158,7 @@ function it_generates_proper_php_code_for_variadics(
) {
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
$class->getProperties()->willReturn(array());
$class->getPropertyNodes()->willReturn(array());
$class->getMethods()->willReturn(array(
$method1, $method2, $method3, $method4
));
Expand Down Expand Up @@ -248,7 +253,7 @@ function it_overrides_properly_methods_with_args_passed_by_reference(
) {
$class->getParentClass()->willReturn('RuntimeException');
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
$class->getProperties()->willReturn(array());
$class->getPropertyNodes()->willReturn(array());
$class->getMethods()->willReturn(array($method));
$class->isReadOnly()->willReturn(false);

Expand Down Expand Up @@ -291,7 +296,7 @@ function it_generates_proper_code_for_union_return_types
{
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn([]);
$class->getProperties()->willReturn([]);
$class->getPropertyNodes()->willReturn([]);
$class->getMethods()->willReturn(array($method));
$class->isReadOnly()->willReturn(false);

Expand Down Expand Up @@ -330,7 +335,7 @@ function it_generates_proper_code_for_union_argument_types
{
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn([]);
$class->getProperties()->willReturn([]);
$class->getPropertyNodes()->willReturn([]);
$class->getMethods()->willReturn(array($method));
$class->isReadOnly()->willReturn(false);

Expand Down Expand Up @@ -370,7 +375,7 @@ function it_generates_empty_class_for_empty_ClassNode(ClassNode $class)
{
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
$class->getProperties()->willReturn(array());
$class->getPropertyNodes()->willReturn(array());
$class->getMethods()->willReturn(array());
$class->isReadOnly()->willReturn(false);

Expand All @@ -391,7 +396,7 @@ function it_wraps_class_in_namespace_if_it_is_namespaced(ClassNode $class)
{
$class->getParentClass()->willReturn('stdClass');
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
$class->getProperties()->willReturn(array());
$class->getPropertyNodes()->willReturn(array());
$class->getMethods()->willReturn(array());
$class->isReadOnly()->willReturn(false);

Expand All @@ -412,7 +417,7 @@ function it_generates_read_only_class_if_parent_class_is_read_only(ClassNode $cl
{
$class->getParentClass()->willReturn('ReadOnlyClass');
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
$class->getProperties()->willReturn(array());
$class->getPropertyNodes()->willReturn(array());
$class->getMethods()->willReturn(array());
$class->isReadOnly()->willReturn(true);

Expand Down
10 changes: 8 additions & 2 deletions spec/Prophecy/Util/StringUtilSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,16 @@ function it_generates_proper_string_representation_for_object(\stdClass $object)
$objHash = sprintf('%s#%s',
get_class($object->getWrappedObject()),
spl_object_id($object->getWrappedObject())
) . " Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n)";
) . " Object (\n" .
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
" 'closure' => Closure#%s Object (\n" .
" 0 => Closure#%s Object\n" .
" )\n" .
" )\n" .
")";

$idRegexExpr = '[0-9]+';
$this->stringify($object)->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr)));
$this->stringify($object)->shouldMatch(sprintf('/^%s$/', sprintf(preg_quote("$objHash"), $idRegexExpr, $idRegexExpr, $idRegexExpr)));
}

function it_generates_proper_string_representation_for_object_without_exporting(\stdClass $object)
Expand Down
21 changes: 15 additions & 6 deletions src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@

namespace Prophecy\Doubler\ClassPatch;

use Prophecy\Doubler\Generator\Node\ArgumentNode;
use Prophecy\Doubler\Generator\Node\ArgumentTypeNode;
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\ArgumentNode;
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
use Prophecy\Doubler\Generator\Node\PropertyTypeNode;

/**
* Add Prophecy functionality to the double.
Expand Down Expand Up @@ -45,10 +45,19 @@ public function supports(ClassNode $node)
public function apply(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
$node->addProperty('objectProphecyClosure', 'private');
$node->addProperty(
'objectProphecyClosureContainer',
'private',
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
);

foreach ($node->getMethods() as $name => $method) {
if ('__construct' === strtolower($name)) {
$method->setCode(
$method->getCode() .
'$this->objectProphecyClosureContainer = new \Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer();'
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be at the beginning in case there's an early return?

);

continue;
}

Expand All @@ -68,16 +77,16 @@ public function apply(ClassNode $node)
$prophecyArgument->setTypeNode(new ArgumentTypeNode('Prophecy\Prophecy\ProphecyInterface'));
$prophecySetter->addArgument($prophecyArgument);
$prophecySetter->setCode(<<<PHP
if (null === \$this->objectProphecyClosure) {
\$this->objectProphecyClosure = static function () use (\$prophecy) {
if (null === \$this->objectProphecyClosureContainer->closure) {
\$this->objectProphecyClosureContainer->closure = static function () use (\$prophecy) {
return \$prophecy;
};
}
PHP
);

$prophecyGetter = new MethodNode('getProphecy');
$prophecyGetter->setCode('return \call_user_func($this->objectProphecyClosure);');
$prophecyGetter->setCode('return \call_user_func($this->objectProphecyClosureContainer->closure);');

if ($node->hasMethod('__call')) {
$__call = $node->getMethod('__call');
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

/*
* This file is part of the Prophecy.
* (c) Konstantin Kudryashov <[email protected]>
* Marcello Duarte <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Prophecy\Doubler\ClassPatch\ProphecySubjectPatch;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this class is part of the final API, it should not be in the ClassPatch namespace at all IMO.

And this class should probably be tagged as @internal

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What defines the final API? Where should I place it instead?


/**
* Container for the closure that can be used and modified in a read-only class.
*
* @internal
*
* @noinspection PhpUnused
*/
class ObjectProphecyClosureContainer
{
/**
* @var \Closure
*/
public $closure = null;
}
22 changes: 19 additions & 3 deletions src/Prophecy/Doubler/Generator/ClassCodeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

namespace Prophecy\Doubler\Generator;

use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
use Prophecy\Doubler\Generator\Node\TypeNodeAbstract;

/**
Expand Down Expand Up @@ -50,8 +49,8 @@ public function generate($classname, Node\ClassNode $class)
)
);

foreach ($class->getProperties() as $name => $visibility) {
$code .= sprintf("%s \$%s;\n", $visibility, $name);
foreach ((array) $class->getPropertyNodes() as $propertyNode) {
$code .= $this->generateProperty($propertyNode)."\n";
}
$code .= "\n";

Expand All @@ -63,6 +62,23 @@ public function generate($classname, Node\ClassNode $class)
return sprintf("namespace %s {\n%s\n}", $namespace, $code);
}

private function generateProperty(Node\PropertyNode $property): string
{
if (PHP_VERSION_ID >= 70400) {
$type = ($type = $this->generateTypes($property->getTypeNode())) ? $type.' ' : '';
} else {
$type = '';
}

$php = sprintf("%s %s%s;",
$property->getVisibility(),
$type,
'$'.$property->getName()
);

return $php;
}

private function generateMethod(Node\MethodNode $method): string
{
$php = sprintf("%s %s function %s%s(%s)%s {\n",
Expand Down
23 changes: 22 additions & 1 deletion src/Prophecy/Doubler/Generator/Node/ClassNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,11 @@ class ClassNode
*/
private $properties = array();

/**
* @var array<string, PropertyNode>
*/
private $propertyNodes = array();

/**
* @var list<string>
*/
Expand Down Expand Up @@ -112,6 +117,14 @@ public function getProperties()
return $this->properties;
}

/**
* @return array<string, PropertyNode>
*/
public function getPropertyNodes()
{
return $this->propertyNodes;
}

/**
* @param string $name
* @param string $visibility
Expand All @@ -120,7 +133,7 @@ public function getProperties()
*
* @phpstan-param 'public'|'private'|'protected' $visibility
*/
public function addProperty($name, $visibility = 'public')
public function addProperty($name, $visibility = 'public', ?PropertyTypeNode $typeNode = null)
{
$visibility = strtolower($visibility);

Expand All @@ -130,6 +143,14 @@ public function addProperty($name, $visibility = 'public')
));
}

$propertyNode = new PropertyNode($name);
$propertyNode->setVisibility($visibility);
if ($typeNode) {
$propertyNode->setTypeNode($typeNode);
}

$this->propertyNodes[$name] = $propertyNode;

$this->properties[$name] = $visibility;
}

Expand Down
Loading