From d2530564ddeedddef636e2bdb4bd1765c2c6119f Mon Sep 17 00:00:00 2001 From: Valentin Wotschel Date: Thu, 25 Apr 2024 14:32:10 +0200 Subject: [PATCH] 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 | 21 ++-- .../ObjectProphecyClosureContainer.php | 27 ++++++ .../Doubler/Generator/ClassCodeGenerator.php | 22 ++++- .../Doubler/Generator/Node/ClassNode.php | 23 ++++- .../Doubler/Generator/Node/PropertyNode.php | 97 +++++++++++++++++++ .../Generator/Node/PropertyTypeNode.php | 17 ++++ src/Prophecy/Util/ExportUtil.php | 2 +- 13 files changed, 255 insertions(+), 29 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 834ecb1d8..d208f4401 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" . + " '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() diff --git a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php index 1c71c6d5d..ba3d59565 100644 --- a/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php +++ b/spec/Prophecy/Argument/Token/IdenticalValueTokenSpec.php @@ -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))); } } diff --git a/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php b/spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php index b4d09082a..567c369f8 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; class ProphecySubjectPatchSpec extends ObjectBehavior @@ -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); @@ -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); diff --git a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php index 50fb1704c..4e1b7bb41 100644 --- a/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php +++ b/spec/Prophecy/Doubler/Generator/ClassCodeGeneratorSpec.php @@ -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 @@ -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); @@ -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 )); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); @@ -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); diff --git a/spec/Prophecy/Util/StringUtilSpec.php b/spec/Prophecy/Util/StringUtilSpec.php index 491cc1a54..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 379bfafd7..d4e6acf70 100644 --- a/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php +++ b/src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php @@ -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. @@ -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();' + ); + continue; } @@ -68,8 +77,8 @@ public function apply(ClassNode $node) $prophecyArgument->setTypeNode(new ArgumentTypeNode('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 +86,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 ddc6985cd..72cfe67de 100644 --- a/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php +++ b/src/Prophecy/Doubler/Generator/ClassCodeGenerator.php @@ -11,7 +11,6 @@ namespace Prophecy\Doubler\Generator; -use Prophecy\Doubler\Generator\Node\ReturnTypeNode; use Prophecy\Doubler\Generator\Node\TypeNodeAbstract; /** @@ -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"; @@ -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", diff --git a/src/Prophecy/Doubler/Generator/Node/ClassNode.php b/src/Prophecy/Doubler/Generator/Node/ClassNode.php index b90b0cc98..d4e3cf27f 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 +{ + +} diff --git a/src/Prophecy/Util/ExportUtil.php b/src/Prophecy/Util/ExportUtil.php index 884585b95..700c07b96 100644 --- a/src/Prophecy/Util/ExportUtil.php +++ b/src/Prophecy/Util/ExportUtil.php @@ -158,7 +158,7 @@ protected static function recursiveExport(&$value, $indentation, $processed = nu '%s %s => %s' . "\n", $whitespace, self::recursiveExport($k, $indentation), - self::recursiveExport($value[$k], $indentation + 1, $processed) + self::recursiveExport($v, $indentation + 1, $processed) ); }