Skip to content

Commit 1553ae4

Browse files
WalterWoshidNek-
authored andcommitted
Add Readonly support
1 parent 8d83b0b commit 1553ae4

13 files changed

Lines changed: 256 additions & 28 deletions

File tree

spec/Prophecy/Argument/ArgumentsWildcardSpec.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,16 @@ function it_wraps_non_token_arguments_into_ExactValueToken(\stdClass $object)
1414
$class = get_class($object->getWrappedObject());
1515
$id = spl_object_id($object->getWrappedObject());
1616

17-
$objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
17+
$objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n" .
18+
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
19+
" 'closure' => Closure#%s Object (\n" .
20+
" 0 => Closure#%s Object\n" .
21+
" )\n" .
22+
" )\n" .
23+
"))";
1824

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

2329
function it_generates_string_representation_from_all_tokens_imploded(

spec/Prophecy/Argument/Token/ExactValueTokenSpec.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,12 +136,18 @@ function it_generates_proper_string_representation_for_object(\stdClass $object)
136136
$objHash = sprintf('exact(%s#%s',
137137
get_class($object->getWrappedObject()),
138138
spl_object_id($object->getWrappedObject())
139-
)." Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
139+
) . " Object (\n" .
140+
" 'objectProphecyClosure' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
141+
" 'closure' => Closure#%s Object (\n" .
142+
" 0 => Closure#%s Object\n" .
143+
" )\n" .
144+
" )\n" .
145+
"))";
140146

141147
$this->beConstructedWith($object);
142148

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

147153
function it_scores_10_if_value_an_numeric_and_equal_to_argument_as_stringable()

spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,11 +144,17 @@ function it_generates_proper_string_representation_for_object($object)
144144
$objHash = sprintf('identical(%s#%s',
145145
get_class($object->getWrappedObject()),
146146
spl_object_id($object->getWrappedObject())
147-
)." Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n))";
147+
) . " Object (\n" .
148+
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
149+
" 'closure' => Closure#%s Object (\n" .
150+
" 0 => Closure#%s Object\n" .
151+
" )\n" .
152+
" )\n" .
153+
"))";
148154

149155
$this->beConstructedWith($object);
150156

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

spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Prophecy\Argument;
77
use Prophecy\Doubler\Generator\Node\ClassNode;
88
use Prophecy\Doubler\Generator\Node\MethodNode;
9+
use Prophecy\Doubler\Generator\Node\PropertyTypeNode;
910
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
1011
use Prophecy\Doubler\Generator\Node\Type\BuiltinType;
1112

@@ -29,8 +30,12 @@ function it_supports_any_class(ClassNode $node)
2930
function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node)
3031
{
3132
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->shouldBeCalled();
33+
$node->addProperty(
34+
'objectProphecyClosureContainer',
35+
'private',
36+
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
37+
)->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));
3238

33-
$node->addProperty('objectProphecyClosure', 'private')->willReturn(null);
3439
$node->getMethods()->willReturn(array());
3540
$node->hasMethod(Argument::any())->willReturn(false);
3641
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);
@@ -48,7 +53,12 @@ function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prop
4853
MethodNode $method4
4954
) {
5055
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->willReturn(null);
51-
$node->addProperty('objectProphecyClosure', 'private')->willReturn(null);
56+
$node->addProperty(
57+
'objectProphecyClosureContainer',
58+
'private',
59+
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
60+
)->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));
61+
5262
$node->hasMethod(Argument::any())->willReturn(false);
5363
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);
5464
$node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null);

spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use Prophecy\Doubler\Generator\Node\ArgumentTypeNode;
1010
use Prophecy\Doubler\Generator\Node\ClassNode;
1111
use Prophecy\Doubler\Generator\Node\MethodNode;
12+
use Prophecy\Doubler\Generator\Node\PropertyNode;
1213
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
1314
use Prophecy\Doubler\Generator\Node\Type\BuiltinType;
1415
use Prophecy\Doubler\Generator\Node\Type\IntersectionType;
@@ -35,7 +36,11 @@ function it_generates_proper_php_code_for_specific_ClassNode(
3536
$class->getInterfaces()->willReturn(array(
3637
'Prophecy\Doubler\Generator\MirroredInterface', 'ArrayAccess', 'ArrayIterator',
3738
));
38-
$class->getProperties()->willReturn(array('name' => 'public', 'email' => 'private'));
39+
$name = new PropertyNode('name');
40+
$name->setVisibility('public');
41+
$email = new PropertyNode('email');
42+
$email->setVisibility('private');
43+
$class->getPropertyNodes()->willReturn(array('name' => $name, 'email' => $email));
3944
$class->getMethods()->willReturn(array($method1, $method2, $method3, $method4, $method5));
4045
$class->isReadOnly()->willReturn(false);
4146

@@ -163,7 +168,7 @@ function it_generates_proper_php_code_for_variadics(
163168
) {
164169
$class->getParentClass()->willReturn('stdClass');
165170
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
166-
$class->getProperties()->willReturn(array());
171+
$class->getPropertyNodes()->willReturn(array());
167172
$class->getMethods()->willReturn(array(
168173
$method1, $method2, $method3, $method4,
169174
));
@@ -258,7 +263,7 @@ function it_overrides_properly_methods_with_args_passed_by_reference(
258263
) {
259264
$class->getParentClass()->willReturn('RuntimeException');
260265
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
261-
$class->getProperties()->willReturn(array());
266+
$class->getPropertyNodes()->willReturn(array());
262267
$class->getMethods()->willReturn(array($method));
263268
$class->isReadOnly()->willReturn(false);
264269

@@ -302,7 +307,7 @@ function it_generates_proper_code_for_union_return_types(
302307
) {
303308
$class->getParentClass()->willReturn('stdClass');
304309
$class->getInterfaces()->willReturn([]);
305-
$class->getProperties()->willReturn([]);
310+
$class->getPropertyNodes()->willReturn([]);
306311
$class->getMethods()->willReturn(array($method));
307312
$class->isReadOnly()->willReturn(false);
308313

@@ -423,7 +428,7 @@ function it_generates_proper_code_for_union_argument_types(
423428
) {
424429
$class->getParentClass()->willReturn('stdClass');
425430
$class->getInterfaces()->willReturn([]);
426-
$class->getProperties()->willReturn([]);
431+
$class->getPropertyNodes()->willReturn([]);
427432
$class->getMethods()->willReturn(array($method));
428433
$class->isReadOnly()->willReturn(false);
429434

@@ -467,7 +472,7 @@ function it_generates_empty_class_for_empty_ClassNode(ClassNode $class)
467472
{
468473
$class->getParentClass()->willReturn('stdClass');
469474
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
470-
$class->getProperties()->willReturn(array());
475+
$class->getPropertyNodes()->willReturn(array());
471476
$class->getMethods()->willReturn(array());
472477
$class->isReadOnly()->willReturn(false);
473478

@@ -488,7 +493,7 @@ function it_wraps_class_in_namespace_if_it_is_namespaced(ClassNode $class)
488493
{
489494
$class->getParentClass()->willReturn('stdClass');
490495
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
491-
$class->getProperties()->willReturn(array());
496+
$class->getPropertyNodes()->willReturn(array());
492497
$class->getMethods()->willReturn(array());
493498
$class->isReadOnly()->willReturn(false);
494499

@@ -509,7 +514,7 @@ function it_generates_read_only_class_if_parent_class_is_read_only(ClassNode $cl
509514
{
510515
$class->getParentClass()->willReturn('ReadOnlyClass');
511516
$class->getInterfaces()->willReturn(array('Prophecy\Doubler\Generator\MirroredInterface'));
512-
$class->getProperties()->willReturn(array());
517+
$class->getPropertyNodes()->willReturn(array());
513518
$class->getMethods()->willReturn(array());
514519
$class->isReadOnly()->willReturn(true);
515520

spec/Prophecy/Util/StringUtilSpec.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,16 @@ function it_generates_proper_string_representation_for_object(\stdClass $object)
7474
$objHash = sprintf('%s#%s',
7575
get_class($object->getWrappedObject()),
7676
spl_object_id($object->getWrappedObject())
77-
)." Object (\n 'objectProphecyClosure' => Closure#%s Object (\n 0 => Closure#%s Object\n )\n)";
77+
) . " Object (\n" .
78+
" 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" .
79+
" 'closure' => Closure#%s Object (\n" .
80+
" 0 => Closure#%s Object\n" .
81+
" )\n" .
82+
" )\n" .
83+
")";
7884

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

8389
function it_generates_proper_string_representation_for_object_without_exporting(\stdClass $object)

src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@
1111

1212
namespace Prophecy\Doubler\ClassPatch;
1313

14+
use Prophecy\Doubler\Generator\Node\ArgumentNode;
1415
use Prophecy\Doubler\Generator\Node\ArgumentTypeNode;
1516
use Prophecy\Doubler\Generator\Node\ClassNode;
1617
use Prophecy\Doubler\Generator\Node\MethodNode;
17-
use Prophecy\Doubler\Generator\Node\ArgumentNode;
1818
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;
1919
use Prophecy\Doubler\Generator\Node\Type\ObjectType;
20+
use Prophecy\Doubler\Generator\Node\PropertyTypeNode;
2021

2122
/**
2223
* Add Prophecy functionality to the double.
@@ -46,10 +47,19 @@ public function supports(ClassNode $node)
4647
public function apply(ClassNode $node)
4748
{
4849
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
49-
$node->addProperty('objectProphecyClosure', 'private');
50+
$node->addProperty(
51+
'objectProphecyClosureContainer',
52+
'private',
53+
new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')
54+
);
5055

5156
foreach ($node->getMethods() as $name => $method) {
5257
if ('__construct' === strtolower($name)) {
58+
$method->setCode(
59+
$method->getCode() .
60+
'$this->objectProphecyClosureContainer = new \Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer();'
61+
);
62+
5363
continue;
5464
}
5565

@@ -69,16 +79,16 @@ public function apply(ClassNode $node)
6979
$prophecyArgument->setTypeNode(new ArgumentTypeNode(new ObjectType('Prophecy\Prophecy\ProphecyInterface')));
7080
$prophecySetter->addArgument($prophecyArgument);
7181
$prophecySetter->setCode(<<<PHP
72-
if (null === \$this->objectProphecyClosure) {
73-
\$this->objectProphecyClosure = static function () use (\$prophecy) {
82+
if (null === \$this->objectProphecyClosureContainer->closure) {
83+
\$this->objectProphecyClosureContainer->closure = static function () use (\$prophecy) {
7484
return \$prophecy;
7585
};
7686
}
7787
PHP
7888
);
7989

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

8393
if ($node->hasMethod('__call')) {
8494
$__call = $node->getMethod('__call');
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Prophecy.
5+
* (c) Konstantin Kudryashov <ever.zet@gmail.com>
6+
* Marcello Duarte <marcello.duarte@gmail.com>
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\ProphecySubjectPatch;
13+
14+
/**
15+
* Container for the closure that can be used and modified in a read-only class.
16+
*
17+
* @internal
18+
*
19+
* @noinspection PhpUnused
20+
*/
21+
class ObjectProphecyClosureContainer
22+
{
23+
/**
24+
* @var \Closure
25+
*/
26+
public $closure = null;
27+
}

src/Prophecy/Doubler/Generator/ClassCodeGenerator.php

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,8 @@ public function generate($classname, Node\ClassNode $class)
5454
)
5555
);
5656

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

@@ -67,6 +67,23 @@ public function generate($classname, Node\ClassNode $class)
6767
return sprintf("namespace %s {\n%s\n}", $namespace, $code);
6868
}
6969

70+
private function generateProperty(Node\PropertyNode $property): string
71+
{
72+
if (PHP_VERSION_ID >= 70400) {
73+
$type = ($type = $this->generateTypes($property->getTypeNode())) ? $type.' ' : '';
74+
} else {
75+
$type = '';
76+
}
77+
78+
$php = sprintf("%s %s%s;",
79+
$property->getVisibility(),
80+
$type,
81+
'$'.$property->getName()
82+
);
83+
84+
return $php;
85+
}
86+
7087
private function generateMethod(Node\MethodNode $method): string
7188
{
7289
$php = sprintf("%s %s function %s%s(%s)%s {\n",

src/Prophecy/Doubler/Generator/Node/ClassNode.php

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ class ClassNode
3737
*/
3838
private $properties = array();
3939

40+
/**
41+
* @var array<string, PropertyNode>
42+
*/
43+
private $propertyNodes = array();
44+
4045
/**
4146
* @var list<string>
4247
*/
@@ -112,6 +117,14 @@ public function getProperties()
112117
return $this->properties;
113118
}
114119

120+
/**
121+
* @return array<string, PropertyNode>
122+
*/
123+
public function getPropertyNodes()
124+
{
125+
return $this->propertyNodes;
126+
}
127+
115128
/**
116129
* @param string $name
117130
* @param string $visibility
@@ -120,7 +133,7 @@ public function getProperties()
120133
*
121134
* @phpstan-param 'public'|'private'|'protected' $visibility
122135
*/
123-
public function addProperty($name, $visibility = 'public')
136+
public function addProperty($name, $visibility = 'public', ?PropertyTypeNode $typeNode = null)
124137
{
125138
$visibility = strtolower($visibility);
126139

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

146+
$propertyNode = new PropertyNode($name);
147+
$propertyNode->setVisibility($visibility);
148+
if ($typeNode) {
149+
$propertyNode->setTypeNode($typeNode);
150+
}
151+
152+
$this->propertyNodes[$name] = $propertyNode;
153+
133154
$this->properties[$name] = $visibility;
134155
}
135156

0 commit comments

Comments
 (0)