From 756da95911c04f84f6dc4a11e74ac4e861f0a666 Mon Sep 17 00:00:00 2001 From: Valentin Wotschel Date: Thu, 25 Apr 2024 14:32:10 +0200 Subject: [PATCH 1/6] Add Readonly support --- .../Argument/ArgumentsWildcardSpec.php | 10 +- .../Argument/Token/ExactValueTokenSpec.php | 10 +- .../Token/IdenticalValueTokenSpec.php | 10 +- .../ClassPatch/ProphecySubjectPatchSpec.php | 14 ++- .../Generator/ClassCodeGeneratorSpec.php | 21 ++-- spec/Prophecy/Util/StringUtilSpec.php | 10 +- .../ClassPatch/ProphecySubjectPatch.php | 20 +++- .../ObjectProphecyClosureContainer.php | 27 ++++++ .../Doubler/Generator/ClassCodeGenerator.php | 21 +++- .../Doubler/Generator/Node/ClassNode.php | 23 ++++- .../Doubler/Generator/Node/PropertyNode.php | 97 +++++++++++++++++++ .../Generator/Node/PropertyTypeNode.php | 17 ++++ 12 files changed, 254 insertions(+), 26 deletions(-) create mode 100644 src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php create mode 100644 src/Prophecy/Doubler/Generator/Node/PropertyNode.php create mode 100644 src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php diff --git a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php index 6c9d52caf..b4f0c7f07 100644 --- a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php +++ b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php @@ -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( diff --git a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php index e090dfde8..c2dcaef4d 100644 --- a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php @@ -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" . + " 'objectProphecyClosure' => 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() diff --git a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php index f25ae3313..de93774dd 100644 --- a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php @@ -143,11 +143,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))); } } diff --git a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php index e6fbedb08..cf70e0441 100644 --- a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php +++ b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php @@ -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; use Prophecy\Doubler\Generator\Node\Type\BuiltinType; @@ -29,8 +30,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); @@ -48,7 +53,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); diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index 4ac65cdd6..3fd40f782 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -7,6 +7,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; use Prophecy\Doubler\Generator\Node\Type\BuiltinType; use Prophecy\Doubler\Generator\Node\Type\IntersectionType; @@ -32,7 +33,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); @@ -160,7 +165,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, )); @@ -255,7 +260,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); @@ -299,7 +304,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); @@ -420,7 +425,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); @@ -464,7 +469,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); @@ -485,7 +490,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); @@ -506,7 +511,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); diff --git a/spec/Prophecy/Util/StringUtilSpec.php b/spec/Prophecy/Util/StringUtilSpec.php index cf0bba7c5..7f1acf3bb 100644 --- a/spec/Prophecy/Util/StringUtilSpec.php +++ b/spec/Prophecy/Util/StringUtilSpec.php @@ -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) diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index 46632fc0c..e70f56822 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -11,11 +11,12 @@ 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\Type\ObjectType; +use Prophecy\Doubler\Generator\Node\PropertyTypeNode; /** * Add Prophecy functionality to the double. @@ -45,10 +46,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();' + ); + continue; } @@ -68,8 +78,8 @@ public function apply(ClassNode $node) $prophecyArgument->setTypeNode(new ArgumentTypeNode(new ObjectType('Prophecy\Prophecy\ProphecyInterface'))); $prophecySetter->addArgument($prophecyArgument); $prophecySetter->setCode(<<objectProphecyClosure) { - \$this->objectProphecyClosure = static function () use (\$prophecy) { +if (null === \$this->objectProphecyClosureContainer->closure) { + \$this->objectProphecyClosureContainer->closure = static function () use (\$prophecy) { return \$prophecy; }; } @@ -77,7 +87,7 @@ public function apply(ClassNode $node) ); $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'); diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php new file mode 100644 index 000000000..5d63e99f0 --- /dev/null +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php @@ -0,0 +1,27 @@ + + * Marcello Duarte + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prophecy\Doubler\ClassPatch\ProphecySubjectPatch; + +/** + * 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; +} diff --git a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php index 5d09c5c7c..1d24e2dbc 100644 --- a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php +++ b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php @@ -47,8 +47,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"; @@ -60,6 +60,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", diff --git a/src/Prophecy/Doubler/Generator/Node/ClassNode.php b/src/Prophecy/Doubler/Generator/Node/ClassNode.php index b69cf6040..17db038ef 100644 --- a/src/Prophecy/Doubler/Generator/Node/ClassNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ClassNode.php @@ -37,6 +37,11 @@ class ClassNode */ private $properties = array(); + /** + * @var array + */ + private $propertyNodes = array(); + /** * @var list */ @@ -112,6 +117,14 @@ public function getProperties() return $this->properties; } + /** + * @return array + */ + public function getPropertyNodes() + { + return $this->propertyNodes; + } + /** * @param string $name * @param string $visibility @@ -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); @@ -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; } diff --git a/src/Prophecy/Doubler/Generator/Node/PropertyNode.php b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php new file mode 100644 index 000000000..57ad95482 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php @@ -0,0 +1,97 @@ + + * Marcello Duarte + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prophecy\Doubler\Generator\Node; + +use Prophecy\Exception\InvalidArgumentException; + +/** + * Property node. + */ +class PropertyNode +{ + private $name; + + /** + * @var string + * + * @phpstan-var 'public'|'private'|'protected' + */ + private $visibility = 'public'; + + /** + * @var PropertyTypeNode + */ + private $typeNode; + + /** + * @param string $name + */ + public function __construct(string $name) + { + $this->name = $name; + $this->typeNode = new PropertyTypeNode(); + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @return PropertyTypeNode + */ + public function getTypeNode(): PropertyTypeNode + { + return $this->typeNode; + } + + /** + * @return void + */ + public function setTypeNode(PropertyTypeNode $typeNode) + { + $this->typeNode = $typeNode; + } + + /** + * @return string + * + * @phpstan-return 'public'|'private'|'protected' + */ + public function getVisibility(): string + { + return $this->visibility; + } + + /** + * @param string $visibility + * + * @return void + * + * @phpstan-param 'public'|'private'|'protected' $visibility + */ + public function setVisibility(string $visibility) + { + $visibility = strtolower($visibility); + + if (!\in_array($visibility, array('public', 'private', 'protected'), true)) { + throw new InvalidArgumentException(sprintf( + '`%s` method visibility is not supported.', $visibility + )); + } + + $this->visibility = $visibility; + } +} diff --git a/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php b/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php new file mode 100644 index 000000000..1317fe634 --- /dev/null +++ b/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php @@ -0,0 +1,17 @@ + + * Marcello Duarte + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Prophecy\Doubler\Generator\Node; + +class PropertyTypeNode extends TypeNodeAbstract +{ + +} From a709e92d01b304937a63c4d9e652a3f0d9ec3fea Mon Sep 17 00:00:00 2001 From: "Maxime Veber (Nek)" Date: Fri, 14 Nov 2025 20:01:11 +0100 Subject: [PATCH 2/6] Change namespace of ObjectProphecyClosureContainer See https://github.com/phpspec/prophecy/pull/623#discussion_r1723281964 --- spec/Prophecy/Argument/ArgumentsWildcardSpec.php | 2 +- spec/Prophecy/Argument/Token/ExactValueTokenSpec.php | 2 +- spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php | 2 +- .../Doubler/ClassPatch/ProphecySubjectPatchSpec.php | 8 ++++---- .../Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php | 4 ++-- spec/Prophecy/Util/StringUtilSpec.php | 2 +- src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php | 4 ++-- .../ObjectProphecyClosureContainer.php | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) rename src/Prophecy/Doubler/{ClassPatch/ProphecySubjectPatch => }/ObjectProphecyClosureContainer.php (89%) diff --git a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php index b4f0c7f07..203fcea95 100644 --- a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php +++ b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php @@ -15,7 +15,7 @@ function it_wraps_non_token_arguments_into_ExactValueToken(\stdClass $object) $id = spl_object_id($object->getWrappedObject()); $objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" . + " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . " 'closure' => Closure#%s Object (\n" . " 0 => Closure#%s Object\n" . " )\n" . diff --git a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php index c2dcaef4d..1c1a2ea83 100644 --- a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php @@ -137,7 +137,7 @@ function it_generates_proper_string_representation_for_object(\stdClass $object) get_class($object->getWrappedObject()), spl_object_id($object->getWrappedObject()) ) . " Object (\n" . - " 'objectProphecyClosure' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" . + " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . " 'closure' => Closure#%s Object (\n" . " 0 => Closure#%s Object\n" . " )\n" . diff --git a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php index de93774dd..e9a1dd41e 100644 --- a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php @@ -144,7 +144,7 @@ function it_generates_proper_string_representation_for_object($object) get_class($object->getWrappedObject()), spl_object_id($object->getWrappedObject()) ) . " Object (\n" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" . + " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . " 'closure' => Closure#%s Object (\n" . " 0 => Closure#%s Object\n" . " )\n" . diff --git a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php index cf70e0441..ab67c3dcc 100644 --- a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php +++ b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php @@ -33,8 +33,8 @@ function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node) $node->addProperty( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer') - )->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')); + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + )->willReturn(Argument::type('Prophecy\Doubler\ObjectProphecyClosureContainer')); $node->getMethods()->willReturn(array()); $node->hasMethod(Argument::any())->willReturn(false); @@ -56,8 +56,8 @@ function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prop $node->addProperty( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer') - )->willReturn(Argument::type('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer')); + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + )->willReturn(Argument::type('Prophecy\Doubler\ObjectProphecyClosureContainer')); $node->hasMethod(Argument::any())->willReturn(false); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null); diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index 3fd40f782..c4f5abb33 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -344,7 +344,7 @@ function it_generates_proper_code_for_dnf_types( ) { $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn([]); - $class->getProperties()->willReturn([]); + $class->getPropertyNodes()->willReturn([]); $class->getMethods()->willReturn(array($method)); $class->isReadOnly()->willReturn(false); @@ -386,7 +386,7 @@ function it_generates_proper_code_for_intersection_return_types( ) { $class->getParentClass()->willReturn('stdClass'); $class->getInterfaces()->willReturn([]); - $class->getProperties()->willReturn([]); + $class->getPropertyNodes()->willReturn([]); $class->getMethods()->willReturn(array($method)); $class->isReadOnly()->willReturn(false); diff --git a/spec/Prophecy/Util/StringUtilSpec.php b/spec/Prophecy/Util/StringUtilSpec.php index 7f1acf3bb..44f2bd3ac 100644 --- a/spec/Prophecy/Util/StringUtilSpec.php +++ b/spec/Prophecy/Util/StringUtilSpec.php @@ -75,7 +75,7 @@ function it_generates_proper_string_representation_for_object(\stdClass $object) get_class($object->getWrappedObject()), spl_object_id($object->getWrappedObject()) ) . " Object (\n" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer#%s Object (\n" . + " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . " 'closure' => Closure#%s Object (\n" . " 0 => Closure#%s Object\n" . " )\n" . diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index e70f56822..88eb9dcc9 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -49,14 +49,14 @@ public function apply(ClassNode $node) $node->addProperty( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer') + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') ); foreach ($node->getMethods() as $name => $method) { if ('__construct' === strtolower($name)) { $method->setCode( $method->getCode() . - '$this->objectProphecyClosureContainer = new \Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer();' + '$this->objectProphecyClosureContainer = new \Prophecy\Doubler\ObjectProphecyClosureContainer();' ); continue; diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php b/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php similarity index 89% rename from src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php rename to src/Prophecy/Doubler/ObjectProphecyClosureContainer.php index 5d63e99f0..de492523e 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch/ObjectProphecyClosureContainer.php +++ b/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Prophecy\Doubler\ClassPatch\ProphecySubjectPatch; +namespace Prophecy\Doubler; /** * Container for the closure that can be used and modified in a read-only class. From 7f3bc1204ff308ffeaa21a144637abd5f928ea92 Mon Sep 17 00:00:00 2001 From: "Maxime Veber (Nek)" Date: Fri, 14 Nov 2025 20:45:09 +0100 Subject: [PATCH 3/6] Update the property node API Changes were asked on the review: https://github.com/phpspec/prophecy/pull/623 --- .../Argument/ArgumentsWildcardSpec.php | 14 ++--- .../Argument/Token/ExactValueTokenSpec.php | 14 ++--- .../Token/IdenticalValueTokenSpec.php | 14 ++--- .../ClassPatch/ProphecySubjectPatchSpec.php | 21 ++++--- .../Doubler/Generator/Node/ClassNodeSpec.php | 12 +++- spec/Prophecy/Util/StringUtilSpec.php | 14 ++--- .../ClassPatch/ProphecySubjectPatch.php | 13 +++-- .../Doubler/Generator/ClassCodeGenerator.php | 8 +-- .../Doubler/Generator/Node/ClassNode.php | 55 +++++++++++-------- .../Doubler/Generator/Node/PropertyNode.php | 28 ++++------ .../Generator/Node/PropertyTypeNode.php | 5 +- 11 files changed, 104 insertions(+), 94 deletions(-) diff --git a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php index 203fcea95..2ee8ecbb1 100644 --- a/spec/Prophecy/Argument/ArgumentsWildcardSpec.php +++ b/spec/Prophecy/Argument/ArgumentsWildcardSpec.php @@ -14,13 +14,13 @@ 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" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . - " 'closure' => Closure#%s Object (\n" . - " 0 => Closure#%s Object\n" . - " )\n" . - " )\n" . - "))"; + $objHash = "exact(42), exact(\"zet\"), exact($class#$id Object (\n" + ." 'objectProphecyClosureContainer' => Prophecy\Doubler\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, $idRegexExpr))); diff --git a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php index 1c1a2ea83..28e279bb8 100644 --- a/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/ExactValueTokenSpec.php @@ -136,13 +136,13 @@ 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" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . - " 'closure' => Closure#%s Object (\n" . - " 0 => Closure#%s Object\n" . - " )\n" . - " )\n" . - "))"; + )." Object (\n" + ." 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" + ." 'closure' => Closure#%s Object (\n" + ." 0 => Closure#%s Object\n" + ." )\n" + ." )\n" + ."))"; $this->beConstructedWith($object); diff --git a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php index e9a1dd41e..6988f811f 100644 --- a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php @@ -143,13 +143,13 @@ 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" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . - " 'closure' => Closure#%s Object (\n" . - " 0 => Closure#%s Object\n" . - " )\n" . - " )\n" . - "))"; + )." Object (\n" + ." 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" + ." 'closure' => Closure#%s Object (\n" + ." 0 => Closure#%s Object\n" + ." )\n" + ." )\n" + ."))"; $this->beConstructedWith($object); diff --git a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php index ab67c3dcc..00dab030a 100644 --- a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php +++ b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php @@ -6,6 +6,7 @@ use Prophecy\Argument; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; +use Prophecy\Doubler\Generator\Node\PropertyNode; use Prophecy\Doubler\Generator\Node\PropertyTypeNode; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; use Prophecy\Doubler\Generator\Node\Type\BuiltinType; @@ -31,10 +32,12 @@ function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->shouldBeCalled(); $node->addProperty( - 'objectProphecyClosureContainer', - 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') - )->willReturn(Argument::type('Prophecy\Doubler\ObjectProphecyClosureContainer')); + new PropertyNode( + 'objectProphecyClosureContainer', + 'private', + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + ) + ); $node->getMethods()->willReturn(array()); $node->hasMethod(Argument::any())->willReturn(false); @@ -54,10 +57,12 @@ function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prop ) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->willReturn(null); $node->addProperty( - 'objectProphecyClosureContainer', - 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') - )->willReturn(Argument::type('Prophecy\Doubler\ObjectProphecyClosureContainer')); + new PropertyNode( + 'objectProphecyClosureContainer', + 'private', + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + ) + ); $node->hasMethod(Argument::any())->willReturn(false); $node->addMethod(Argument::type('Prophecy\Doubler\Generator\Node\MethodNode'), true)->willReturn(null); diff --git a/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php index fbf1a99cf..9798ab75c 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php @@ -4,6 +4,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Doubler\Generator\Node\MethodNode; +use Prophecy\Doubler\Generator\Node\PropertyNode; use Prophecy\Exception\Doubler\MethodNotExtendableException; class ClassNodeSpec extends ObjectBehavior @@ -120,7 +121,7 @@ function it_does_not_have_properties_by_default() $this->getProperties()->shouldHaveCount(0); } - function it_is_able_to_have_properties() + function it_is_able_to_have_properties_deprecated_api() { $this->addProperty('title'); $this->addProperty('text', 'private'); @@ -130,6 +131,15 @@ function it_is_able_to_have_properties() )); } + function it_is_able_to_have_properties() + { + $prop1 = new PropertyNode('title'); + $prop2 = new PropertyNode('text', 'private'); + $this->addProperty($prop1); + $this->addProperty($prop2); + $this->getPropertyNodes()->shouldReturn(['title' => $prop1, 'text' => $prop2]); + } + function its_addProperty_does_not_accept_unsupported_visibility() { $this->shouldThrow('InvalidArgumentException')->duringAddProperty('title', 'town'); diff --git a/spec/Prophecy/Util/StringUtilSpec.php b/spec/Prophecy/Util/StringUtilSpec.php index 44f2bd3ac..268974134 100644 --- a/spec/Prophecy/Util/StringUtilSpec.php +++ b/spec/Prophecy/Util/StringUtilSpec.php @@ -74,13 +74,13 @@ 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" . - " 'objectProphecyClosureContainer' => Prophecy\Doubler\ObjectProphecyClosureContainer#%s Object (\n" . - " 'closure' => Closure#%s Object (\n" . - " 0 => Closure#%s Object\n" . - " )\n" . - " )\n" . - ")"; + )." Object (\n" + ." 'objectProphecyClosureContainer' => Prophecy\Doubler\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, $idRegexExpr))); diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index 88eb9dcc9..6b441abb4 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -15,8 +15,9 @@ use Prophecy\Doubler\Generator\Node\ArgumentTypeNode; use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; -use Prophecy\Doubler\Generator\Node\Type\ObjectType; +use Prophecy\Doubler\Generator\Node\PropertyNode; use Prophecy\Doubler\Generator\Node\PropertyTypeNode; +use Prophecy\Doubler\Generator\Node\Type\ObjectType; /** * Add Prophecy functionality to the double. @@ -47,16 +48,18 @@ public function apply(ClassNode $node) { $node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface'); $node->addProperty( - 'objectProphecyClosureContainer', - 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + new PropertyNode( + 'objectProphecyClosureContainer', + 'private', + new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + ) ); foreach ($node->getMethods() as $name => $method) { if ('__construct' === strtolower($name)) { $method->setCode( - $method->getCode() . '$this->objectProphecyClosureContainer = new \Prophecy\Doubler\ObjectProphecyClosureContainer();' + .$method->getCode() ); continue; diff --git a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php index 1d24e2dbc..78d916952 100644 --- a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php +++ b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php @@ -47,7 +47,7 @@ public function generate($classname, Node\ClassNode $class) ) ); - foreach ((array) $class->getPropertyNodes() as $propertyNode) { + foreach ($class->getPropertyNodes() as $propertyNode) { $code .= $this->generateProperty($propertyNode)."\n"; } $code .= "\n"; @@ -62,11 +62,7 @@ public function generate($classname, Node\ClassNode $class) private function generateProperty(Node\PropertyNode $property): string { - if (PHP_VERSION_ID >= 70400) { - $type = ($type = $this->generateTypes($property->getTypeNode())) ? $type.' ' : ''; - } else { - $type = ''; - } + $type = ($type = $this->generateTypes($property->getTypeNode())) ? $type.' ' : ''; $php = sprintf("%s %s%s;", $property->getVisibility(), diff --git a/src/Prophecy/Doubler/Generator/Node/ClassNode.php b/src/Prophecy/Doubler/Generator/Node/ClassNode.php index 17db038ef..dd4a4d3e1 100644 --- a/src/Prophecy/Doubler/Generator/Node/ClassNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ClassNode.php @@ -30,17 +30,10 @@ class ClassNode */ private $interfaces = array(); - /** - * @var array - * - * @phpstan-var array - */ - private $properties = array(); - /** * @var array */ - private $propertyNodes = array(); + private $properties = []; /** * @var list @@ -111,30 +104,49 @@ public function hasInterface($interface) * @return array * * @phpstan-return array + * + * @deprecated */ public function getProperties() { - return $this->properties; + trigger_deprecation( + 'phpspec/prophecy', + '1.24', + 'Use getPropertyNodes() instead. It allows you to retrieve the type as well as visibility.' + ); + + $propertiesOldFormat = array_map(function ($property) { + return $property->getVisibility(); + }, $this->properties); + + return $propertiesOldFormat; } /** * @return array */ - public function getPropertyNodes() + public function getPropertyNodes(): array { - return $this->propertyNodes; + return $this->properties; } /** - * @param string $name - * @param string $visibility - * - * @return void - * * @phpstan-param 'public'|'private'|'protected' $visibility */ - public function addProperty($name, $visibility = 'public', ?PropertyTypeNode $typeNode = null) + public function addProperty(PropertyNode|string $property, string $visibility = 'public'): void { + if ($property instanceof PropertyNode) { + $this->properties[$property->getName()] = $property; + + return; + } + + trigger_deprecation( + 'phpspec/prophecy', + '1.24', + 'The method addProperty() now expects a PropertyNode object instead of a string' + ); + $visibility = strtolower($visibility); if (!\in_array($visibility, array('public', 'private', 'protected'), true)) { @@ -143,15 +155,10 @@ public function addProperty($name, $visibility = 'public', ?PropertyTypeNode $ty )); } - $propertyNode = new PropertyNode($name); + $propertyNode = new PropertyNode($property); $propertyNode->setVisibility($visibility); - if ($typeNode) { - $propertyNode->setTypeNode($typeNode); - } - - $this->propertyNodes[$name] = $propertyNode; - $this->properties[$name] = $visibility; + $this->properties[$property] = $propertyNode; } /** diff --git a/src/Prophecy/Doubler/Generator/Node/PropertyNode.php b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php index 57ad95482..9480a8097 100644 --- a/src/Prophecy/Doubler/Generator/Node/PropertyNode.php +++ b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php @@ -18,27 +18,21 @@ */ class PropertyNode { - private $name; + private string $name; - /** - * @var string - * - * @phpstan-var 'public'|'private'|'protected' - */ - private $visibility = 'public'; + /** @phpstan-var 'public'|'private'|'protected' */ + private string $visibility; - /** - * @var PropertyTypeNode - */ - private $typeNode; + private PropertyTypeNode $typeNode; /** - * @param string $name + * @phpstan-param 'public'|'private'|'protected' $visibility */ - public function __construct(string $name) + public function __construct(string $name, string $visibility = 'public', PropertyTypeNode $typeNode = new PropertyTypeNode()) { $this->name = $name; - $this->typeNode = new PropertyTypeNode(); + $this->setVisibility($visibility); + $this->typeNode = $typeNode; } /** @@ -60,7 +54,7 @@ public function getTypeNode(): PropertyTypeNode /** * @return void */ - public function setTypeNode(PropertyTypeNode $typeNode) + public function setTypeNode(PropertyTypeNode $typeNode): void { $this->typeNode = $typeNode; } @@ -82,10 +76,8 @@ public function getVisibility(): string * * @phpstan-param 'public'|'private'|'protected' $visibility */ - public function setVisibility(string $visibility) + public function setVisibility(string $visibility): void { - $visibility = strtolower($visibility); - if (!\in_array($visibility, array('public', 'private', 'protected'), true)) { throw new InvalidArgumentException(sprintf( '`%s` method visibility is not supported.', $visibility diff --git a/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php b/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php index 1317fe634..fd7c4d5b7 100644 --- a/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php +++ b/src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php @@ -11,7 +11,4 @@ namespace Prophecy\Doubler\Generator\Node; -class PropertyTypeNode extends TypeNodeAbstract -{ - -} +class PropertyTypeNode extends TypeNodeAbstract {} From e865307b2d37d5eeb8ff36880ce4bc737edecb00 Mon Sep 17 00:00:00 2001 From: "Maxime Veber (Nek)" Date: Fri, 21 Nov 2025 19:38:01 +0100 Subject: [PATCH 4/6] Add non regression test for readonly classes --- fixtures/ReadOnlyClass.php | 6 ++++++ phpunit.xml.dist | 1 + tests/FunctionalTest.php | 11 +++++++++++ 3 files changed, 18 insertions(+) diff --git a/fixtures/ReadOnlyClass.php b/fixtures/ReadOnlyClass.php index dce31f4a6..1140259d3 100644 --- a/fixtures/ReadOnlyClass.php +++ b/fixtures/ReadOnlyClass.php @@ -4,4 +4,10 @@ readonly class ReadOnlyClass { + public int $foo; + + public function __construct() + { + $this->foo = 1; + } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ad02f2218..ffc7de397 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -2,6 +2,7 @@ + diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 5d195fce1..82137aaef 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -2,10 +2,12 @@ namespace Tests\Prophecy; +use Fixtures\Prophecy\ReadOnlyClass; use Fixtures\Prophecy\ReturningFinalClass; use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Prophecy\Doubler\DoubleInterface; +use Prophecy\Doubler\Generator\ClassMirror; use Prophecy\Exception\Prophecy\MethodProphecyException; use Prophecy\Prophecy\ProphecySubjectInterface; use Prophecy\Prophet; @@ -59,4 +61,13 @@ public function testUnconfiguredFinalReturnType(): void $double->doSomething(); } + + #[Test] + public function it_can_prophet_readonly_classes(): void + { + $prophet = new Prophet(); + $object = $prophet->prophesize(ReadOnlyClass::class); + + $this->assertInstanceOf(ReadOnlyClass::class, $object->reveal()); + } } From 8fcd94f6907def406b2c406614814a2b8d47f868 Mon Sep 17 00:00:00 2001 From: "Maxime Veber (Nek)" Date: Sat, 21 Mar 2026 20:04:27 +0100 Subject: [PATCH 5/6] Review #659 - Add support for readonly properties --- .../ClassPatch/ProphecySubjectPatchSpec.php | 6 +++--- .../Generator/ClassCodeGeneratorSpec.php | 8 ++++---- .../Doubler/Generator/Node/ClassNodeSpec.php | 5 +++-- .../ClassPatch/ProphecySubjectPatch.php | 3 +-- .../Doubler/Generator/ClassCodeGenerator.php | 2 +- .../Doubler/Generator/Node/ClassNode.php | 4 ++-- .../Doubler/Generator/Node/PropertyNode.php | 19 ++++++------------- .../ObjectProphecyClosureContainer.php | 2 +- tests/FunctionalTest.php | 1 - 9 files changed, 21 insertions(+), 29 deletions(-) diff --git a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php index 00dab030a..0881b77b5 100644 --- a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php +++ b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php @@ -7,9 +7,9 @@ use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\PropertyNode; -use Prophecy\Doubler\Generator\Node\PropertyTypeNode; use Prophecy\Doubler\Generator\Node\ReturnTypeNode; use Prophecy\Doubler\Generator\Node\Type\BuiltinType; +use Prophecy\Doubler\Generator\Node\Type\ObjectType; class ProphecySubjectPatchSpec extends ObjectBehavior { @@ -35,7 +35,7 @@ function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node) new PropertyNode( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + new ObjectType('Prophecy\Doubler\ObjectProphecyClosureContainer') ) ); @@ -60,7 +60,7 @@ function it_forces_all_class_methods_except_constructor_to_proxy_calls_into_prop new PropertyNode( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + new ObjectType('Prophecy\Doubler\ObjectProphecyClosureContainer') ) ); diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index c4f5abb33..103cd1a9f 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -33,9 +33,9 @@ function it_generates_proper_php_code_for_specific_ClassNode( $class->getInterfaces()->willReturn(array( 'Prophecy\Doubler\Generator\MirroredInterface', 'ArrayAccess', 'ArrayIterator', )); - $name = new PropertyNode('name'); + $name = new PropertyNode('name', 'public', new BuiltinType('mixed')); $name->setVisibility('public'); - $email = new PropertyNode('email'); + $email = new PropertyNode('email', 'public', new BuiltinType('mixed')); $email->setVisibility('private'); $class->getPropertyNodes()->willReturn(array('name' => $name, 'email' => $email)); $class->getMethods()->willReturn(array($method1, $method2, $method3, $method4, $method5)); @@ -125,8 +125,8 @@ function it_generates_proper_php_code_for_specific_ClassNode( $expected = <<<'PHP' namespace { class CustomClass extends \RuntimeException implements \Prophecy\Doubler\Generator\MirroredInterface, \ArrayAccess, \ArrayIterator { -public $name; -private $email; +public mixed $name; +private mixed $email; public static function getName(array $fullname, \ReflectionClass $class, object $instance): string|null { return $this->name; diff --git a/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php b/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php index 9798ab75c..50ee8ef40 100644 --- a/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php +++ b/spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php @@ -5,6 +5,7 @@ use PhpSpec\ObjectBehavior; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\PropertyNode; +use Prophecy\Doubler\Generator\Node\Type\BuiltinType; use Prophecy\Exception\Doubler\MethodNotExtendableException; class ClassNodeSpec extends ObjectBehavior @@ -133,8 +134,8 @@ function it_is_able_to_have_properties_deprecated_api() function it_is_able_to_have_properties() { - $prop1 = new PropertyNode('title'); - $prop2 = new PropertyNode('text', 'private'); + $prop1 = new PropertyNode('title', 'public', new BuiltinType('string')); + $prop2 = new PropertyNode('text', 'private', new BuiltinType('string')); $this->addProperty($prop1); $this->addProperty($prop2); $this->getPropertyNodes()->shouldReturn(['title' => $prop1, 'text' => $prop2]); diff --git a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php index 6b441abb4..84294c502 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -16,7 +16,6 @@ use Prophecy\Doubler\Generator\Node\ClassNode; use Prophecy\Doubler\Generator\Node\MethodNode; use Prophecy\Doubler\Generator\Node\PropertyNode; -use Prophecy\Doubler\Generator\Node\PropertyTypeNode; use Prophecy\Doubler\Generator\Node\Type\ObjectType; /** @@ -51,7 +50,7 @@ public function apply(ClassNode $node) new PropertyNode( 'objectProphecyClosureContainer', 'private', - new PropertyTypeNode('Prophecy\Doubler\ObjectProphecyClosureContainer') + new ObjectType('Prophecy\Doubler\ObjectProphecyClosureContainer') ) ); diff --git a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php index 78d916952..ed070b5d5 100644 --- a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php +++ b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php @@ -62,7 +62,7 @@ public function generate($classname, Node\ClassNode $class) private function generateProperty(Node\PropertyNode $property): string { - $type = ($type = $this->generateTypes($property->getTypeNode())) ? $type.' ' : ''; + $type = ($type = ((string) $property->getTypeNode())) ? $type.' ' : ''; $php = sprintf("%s %s%s;", $property->getVisibility(), diff --git a/src/Prophecy/Doubler/Generator/Node/ClassNode.php b/src/Prophecy/Doubler/Generator/Node/ClassNode.php index dd4a4d3e1..d8c2c5528 100644 --- a/src/Prophecy/Doubler/Generator/Node/ClassNode.php +++ b/src/Prophecy/Doubler/Generator/Node/ClassNode.php @@ -11,6 +11,7 @@ namespace Prophecy\Doubler\Generator\Node; +use Prophecy\Doubler\Generator\Node\Type\BuiltinType; use Prophecy\Exception\Doubler\MethodNotExtendableException; use Prophecy\Exception\InvalidArgumentException; @@ -155,8 +156,7 @@ public function addProperty(PropertyNode|string $property, string $visibility = )); } - $propertyNode = new PropertyNode($property); - $propertyNode->setVisibility($visibility); + $propertyNode = new PropertyNode($property, $visibility, new BuiltinType('mixed')); $this->properties[$property] = $propertyNode; } diff --git a/src/Prophecy/Doubler/Generator/Node/PropertyNode.php b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php index 9480a8097..2c2bef584 100644 --- a/src/Prophecy/Doubler/Generator/Node/PropertyNode.php +++ b/src/Prophecy/Doubler/Generator/Node/PropertyNode.php @@ -11,24 +11,25 @@ namespace Prophecy\Doubler\Generator\Node; +use Prophecy\Doubler\Generator\Node\Type\TypeInterface; use Prophecy\Exception\InvalidArgumentException; /** * Property node. */ -class PropertyNode +final class PropertyNode { private string $name; /** @phpstan-var 'public'|'private'|'protected' */ private string $visibility; - private PropertyTypeNode $typeNode; + private TypeInterface $typeNode; /** * @phpstan-param 'public'|'private'|'protected' $visibility */ - public function __construct(string $name, string $visibility = 'public', PropertyTypeNode $typeNode = new PropertyTypeNode()) + public function __construct(string $name, string $visibility, TypeInterface $typeNode) { $this->name = $name; $this->setVisibility($visibility); @@ -44,21 +45,13 @@ public function getName(): string } /** - * @return PropertyTypeNode + * @return TypeInterface */ - public function getTypeNode(): PropertyTypeNode + public function getTypeNode(): TypeInterface { return $this->typeNode; } - /** - * @return void - */ - public function setTypeNode(PropertyTypeNode $typeNode): void - { - $this->typeNode = $typeNode; - } - /** * @return string * diff --git a/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php b/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php index de492523e..346e70fe5 100644 --- a/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php +++ b/src/Prophecy/Doubler/ObjectProphecyClosureContainer.php @@ -18,7 +18,7 @@ * * @noinspection PhpUnused */ -class ObjectProphecyClosureContainer +final class ObjectProphecyClosureContainer { /** * @var \Closure diff --git a/tests/FunctionalTest.php b/tests/FunctionalTest.php index 82137aaef..d387df8c4 100644 --- a/tests/FunctionalTest.php +++ b/tests/FunctionalTest.php @@ -7,7 +7,6 @@ use PHPUnit\Framework\Attributes\Test; use PHPUnit\Framework\TestCase; use Prophecy\Doubler\DoubleInterface; -use Prophecy\Doubler\Generator\ClassMirror; use Prophecy\Exception\Prophecy\MethodProphecyException; use Prophecy\Prophecy\ProphecySubjectInterface; use Prophecy\Prophet; From 5d11850f23271b8d0408660637058ea34552eeac Mon Sep 17 00:00:00 2001 From: "Maxime Veber (Nek)" Date: Sat, 21 Mar 2026 20:25:06 +0100 Subject: [PATCH 6/6] Update baseline --- phpstan-baseline.neon | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7032f1ecc..b2f722161 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,11 +1,23 @@ parameters: ignoreErrors: - - message: '#^Parameter \#1 \$value of function strval expects bool\|float\|int\|resource\|string\|null, mixed given\.$#' + message: '#^Parameter \#1 \$value of function strval expects bool\|float\|GMP\|int\|resource\|string\|null, mixed given\.$#' identifier: argument.type count: 2 path: src/Prophecy/Argument/Token/ExactValueToken.php + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Prophecy/Argument/Token/InArrayToken.php + + - + message: '#^Parameter \#2 \$array of function implode expects array\, array\ given\.$#' + identifier: argument.type + count: 1 + path: src/Prophecy/Argument/Token/NotInArrayToken.php + - message: '#^Method Prophecy\\Doubler\\CachedDoubler\:\:createDoubleClass\(\) should return class\-string\ but returns class\-string\.$#' identifier: return.type