Skip to content

Commit

Permalink
Add Readonly support
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin Wotschel authored and WalterWoshid committed Aug 20, 2024
1 parent f9e07be commit d253056
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 29 deletions.
10 changes: 8 additions & 2 deletions spec/Prophecy/Argument/ArgumentsWildcardSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,16 @@ function it_wraps_non_token_arguments_into_ExactValueToken(\stdClass $object)
$class = get_class($object->getWrappedObject());
$id = spl_object_id($object->getWrappedObject());

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

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

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

$this->beConstructedWith($object);

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

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

$this->beConstructedWith($object);

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

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

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

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

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

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

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

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

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

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

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

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

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

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

namespace Prophecy\Doubler\ClassPatch;

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

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

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

continue;
}

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

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

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

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

namespace Prophecy\Doubler\ClassPatch\ProphecySubjectPatch;

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

namespace Prophecy\Doubler\Generator;

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

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

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

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

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

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

return $php;
}

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

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

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

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

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

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

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

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

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

Expand Down
Loading

0 comments on commit d253056

Please sign in to comment.