Skip to content

Commit

Permalink
Add Readonly support
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin Wotschel committed Apr 25, 2024
1 parent f9e07be commit d909e7e
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 33 deletions.
15 changes: 13 additions & 2 deletions spec/Prophecy/Doubler/ClassPatch/ProphecySubjectPatchSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
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;

class ProphecySubjectPatchSpec extends ObjectBehavior
Expand All @@ -29,7 +31,11 @@ function it_forces_class_to_implement_ProphecySubjectInterface(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface')->shouldBeCalled();

$node->addProperty('objectProphecyClosure', 'private')->willReturn(null);
$objectProphecyClosureContainer = new PropertyNode('objectProphecyClosureContainer');
$objectProphecyClosureContainer->setTypeNode(new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));
$objectProphecyClosureContainer->setVisibility('private');
$node->addProperty($objectProphecyClosureContainer)->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 +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);

$objectProphecyClosureContainer = new PropertyNode('objectProphecyClosureContainer');
$objectProphecyClosureContainer->setTypeNode(new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));
$objectProphecyClosureContainer->setVisibility('private');
$node->addProperty($objectProphecyClosureContainer)->willReturn(null);

$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
8 changes: 7 additions & 1 deletion 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,12 @@ 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'));

$nameProperty = new PropertyNode('name');
$emailProperty = new PropertyNode('email');
$emailProperty->setVisibility('private');
$class->getProperties()->willReturn(array('name' => $nameProperty, 'email' => $emailProperty));

$class->getMethods()->willReturn(array($method1, $method2, $method3, $method4, $method5));
$class->isReadOnly()->willReturn(false);

Expand Down
16 changes: 12 additions & 4 deletions spec/Prophecy/Doubler/Generator/Node/ClassNodeSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -122,8 +123,12 @@ function it_does_not_have_properties_by_default()

function it_is_able_to_have_properties()
{
$this->addProperty('title');
$this->addProperty('text', 'private');
$this->addProperty(new PropertyNode('title'));

$textProperty = new PropertyNode('text');
$textProperty->setVisibility('private');
$this->addProperty($textProperty);

$this->getProperties()->shouldReturn(array(
'title' => 'public',
'text' => 'private'
Expand All @@ -137,8 +142,11 @@ function its_addProperty_does_not_accept_unsupported_visibility()

function its_addProperty_lowercases_visibility_before_setting()
{
$this->addProperty('text', 'PRIVATE');
$this->getProperties()->shouldReturn(array('text' => 'private'));
$textProperty = new PropertyNode('text');
$textProperty->setVisibility('PRIVATE');
$this->addProperty($textProperty);

$this->getProperties()->shouldReturn(array('text' => $textProperty));
}

function its_has_no_unextendable_methods_by_default()
Expand Down
20 changes: 16 additions & 4 deletions src/Prophecy/Doubler/ClassPatch/ProphecySubjectPatch.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
use Prophecy\Doubler\Generator\Node\ClassNode;
use Prophecy\Doubler\Generator\Node\MethodNode;
use Prophecy\Doubler\Generator\Node\ArgumentNode;
use Prophecy\Doubler\Generator\Node\PropertyNode;
use Prophecy\Doubler\Generator\Node\PropertyTypeNode;
use Prophecy\Doubler\Generator\Node\ReturnTypeNode;

/**
Expand Down Expand Up @@ -45,10 +47,20 @@ public function supports(ClassNode $node)
public function apply(ClassNode $node)
{
$node->addInterface('Prophecy\Prophecy\ProphecySubjectInterface');
$node->addProperty('objectProphecyClosure', 'private');

$objectProphecyClosureContainer = new PropertyNode('objectProphecyClosureContainer');
$objectProphecyClosureContainer->setTypeNode(new PropertyTypeNode('Prophecy\Doubler\ClassPatch\ProphecySubjectPatch\ObjectProphecyClosureContainer'));
$objectProphecyClosureContainer->setVisibility('private');

$node->addProperty($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 +80,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,22 @@
<?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.
*
* @noinspection PhpUnused
*/
class ObjectProphecyClosureContainer
{
public $closure = null;
}
21 changes: 19 additions & 2 deletions src/Prophecy/Doubler/Generator/ClassCodeGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,8 @@ public function generate($classname, Node\ClassNode $class)
)
);

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

Expand All @@ -63,6 +63,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;\n",
$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
25 changes: 5 additions & 20 deletions src/Prophecy/Doubler/Generator/Node/ClassNode.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,7 @@ class ClassNode
private $interfaces = array();

/**
* @var array<string, string>
*
* @phpstan-var array<string, 'public'|'private'|'protected'>
* @var array<string, PropertyNode>
*/
private $properties = array();

Expand Down Expand Up @@ -103,34 +101,21 @@ public function hasInterface($interface)
}

/**
* @return array<string, string>
*
* @phpstan-return array<string, 'public'|'private'|'protected'>
* @return array<string, PropertyNode>
*/
public function getProperties()
{
return $this->properties;
}

/**
* @param string $name
* @param string $visibility
* @param PropertyNode $property
*
* @return void
*
* @phpstan-param 'public'|'private'|'protected' $visibility
*/
public function addProperty($name, $visibility = 'public')
public function addProperty(PropertyNode $property): void
{
$visibility = strtolower($visibility);

if (!\in_array($visibility, array('public', 'private', 'protected'), true)) {
throw new InvalidArgumentException(sprintf(
'`%s` property visibility is not supported.', $visibility
));
}

$this->properties[$name] = $visibility;
$this->properties[$property->getName()] = $property;
}

/**
Expand Down
94 changes: 94 additions & 0 deletions src/Prophecy/Doubler/Generator/Node/PropertyNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?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\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;
}

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;
}
}
17 changes: 17 additions & 0 deletions src/Prophecy/Doubler/Generator/Node/PropertyTypeNode.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?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\Generator\Node;

class PropertyTypeNode extends TypeNodeAbstract
{

}

0 comments on commit d909e7e

Please sign in to comment.