Completed
Push — master ( 725e76...e58edb )
by Marco
04:59
created

findAllInstanceProperties()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 16
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 3

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 16
ccs 9
cts 9
cp 1
rs 9.4285
cc 3
eloc 9
nc 2
nop 1
crap 3
1
<?php
2
/*
3
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
4
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
5
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
6
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
7
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
8
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
9
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
10
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
11
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
12
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
13
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
14
 *
15
 * This software consists of voluntary contributions made by many individuals
16
 * and is licensed under the MIT license.
17
 */
18
19
declare(strict_types=1);
20
21
namespace GeneratedHydrator\CodeGenerator\Visitor;
22
23
use PhpParser\Node;
24
use PhpParser\Node\Expr\Array_;
25
use PhpParser\Node\Param;
26
use PhpParser\Node\Stmt\Class_;
27
use PhpParser\Node\Stmt\ClassMethod;
28
use PhpParser\Node\Stmt\Property;
29
use PhpParser\Node\Stmt\PropertyProperty;
30
use PhpParser\NodeVisitorAbstract;
31
use PhpParser\ParserFactory;
32
use ReflectionClass;
33
34
/**
35
 * Replaces methods `__construct`, `hydrate` and `extract` in the classes of the given AST
36
 *
37
 * @todo as per https://github.com/Ocramius/GeneratedHydrator/pull/59, using a visitor for this is ineffective.
38
 * @todo Instead, we can just create a code generator, since we are not modifying code, but creating it.
39
 *
40
 * @author Marco Pivetta <[email protected]>
41
 * @author Pierre Rineau <[email protected]>
42
 * @license MIT
43
 */
44
class HydratorMethodsVisitor extends NodeVisitorAbstract
45
{
46
    /**
47
     * @var string[]
48
     */
49
    private $visiblePropertyMap = array();
50
51
    /**
52
     * @var string[][]
53
     */
54
    private $hiddenPropertyMap = array();
55
56
    /**
57
     * @param ReflectionClass $reflectedClass
58
     */
59 2
    public function __construct(ReflectionClass $reflectedClass)
60
    {
61 2
        foreach ($this->findAllInstanceProperties($reflectedClass) as $property) {
62 2
            $className = $property->getDeclaringClass()->getName();
0 ignored issues
show
introduced by
Consider using $property->class. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
63
64 2
            if ($property->isPrivate() || $property->isProtected()) {
65 2
                $this->hiddenPropertyMap[$className][] = $property->getName();
66
            } else {
67 2
                $this->visiblePropertyMap[] = $property->getName();
68
            }
69
        }
70
    }
71
72
    /**
73
     * @param Node $node
74 2
     *
75
     * @return null|Class_
76 2
     */
77
    public function leaveNode(Node $node)
78
    {
79
        if (! $node instanceof Class_) {
80 2
            return null;
81 2
        }
82 2
83
        $node->stmts[] = new Property(Class_::MODIFIER_PRIVATE, array(
84 2
            new PropertyProperty('hydrateCallbacks', new Array_()),
85
            new PropertyProperty('extractCallbacks', new Array_()),
86
        ));
87
88
        $this->replaceConstructor($this->findOrCreateMethod($node, '__construct'));
0 ignored issues
show
Deprecated Code introduced by
The method GeneratedHydrator\CodeGe...r::findOrCreateMethod() has been deprecated with message: not needed if we move away from code replacement

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
89
        $this->replaceHydrate($this->findOrCreateMethod($node, 'hydrate'));
0 ignored issues
show
Deprecated Code introduced by
The method GeneratedHydrator\CodeGe...r::findOrCreateMethod() has been deprecated with message: not needed if we move away from code replacement

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
90 2
        $this->replaceExtract($this->findOrCreateMethod($node, 'extract'));
0 ignored issues
show
Deprecated Code introduced by
The method GeneratedHydrator\CodeGe...r::findOrCreateMethod() has been deprecated with message: not needed if we move away from code replacement

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
91
92 2
        return $node;
93
    }
94 2
95
    /**
96 2
     * Find all class properties recursively using class hierarchy without
97 2
     * removing name redefinitions
98 2
     *
99 2
     * @param \ReflectionClass $class
100 2
     *
101
     * @return \ReflectionProperty[]
102 2
     */
103 2
    private function findAllInstanceProperties(?\ReflectionClass $class)
104 2
    {
105
        if (! $class) {
106
            return [];
107 2
        }
108 2
109 2
        return array_values(array_merge(
110 2
            $this->findAllInstanceProperties($class->getParentClass() ?: null), // of course PHP is shit.
0 ignored issues
show
Documentation introduced by
$class->getParentClass() ?: null is of type object|null, but the function expects a object<ReflectionClass>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
111
            array_values(array_filter(
112
                $class->getProperties(),
113
                function (\ReflectionProperty $property) : bool {
114
                    return ! $property->isStatic();
115 2
                }
116
            ))
117 2
        ));
118 2
    }
119 2
120
    /**
121
     * @param ClassMethod $method
122 2
     */
123
    private function replaceConstructor(ClassMethod $method)
124 2
    {
125
        $method->params = array();
126 1
127 1
        $bodyParts = array();
128 1
129 1
        // Create a set of closures that will be called to hydrate the object.
130
        // Array of closures in a naturally indexed array, ordered, which will
131
        // then be called in order in the hydrate() and extract() methods.
132 2
        foreach ($this->hiddenPropertyMap as $className => $propertyNames) {
133
            // Hydrate closures
134 2
            $bodyParts[] = "\$this->hydrateCallbacks[] = \\Closure::bind(function (\$object, \$values) {";
135 2
            foreach ($propertyNames as $propertyName) {
136 2
                $bodyParts[] = "    if (isset(\$values['" . $propertyName . "'])) {";
137 2
                $bodyParts[] = "        \$object->" . $propertyName . " = \$values['" . $propertyName . "'];";
138
                $bodyParts[] = "    }";
139
            }
140 2
            $bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n";
141
142 2
            // Extract closures
143 2
            $bodyParts[] = "\$this->extractCallbacks[] = \\Closure::bind(function (\$object, &\$values) {";
144 2
            foreach ($propertyNames as $propertyName) {
145 2
                $bodyParts[] = "    \$values['" . $propertyName . "'] = \$object->" . $propertyName . ";";
146
            }
147
            $bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n";
148
        }
149
150
        $method->stmts = (new ParserFactory)
0 ignored issues
show
Documentation Bug introduced by
It seems like (new \PhpParser\ParserFa...plode(' ', $bodyParts)) can be null. However, the property $stmts is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
151
            ->create(ParserFactory::ONLY_PHP7)
152 2
            ->parse('<?php ' . implode("\n", $bodyParts));
153
    }
154 2
155
    /**
156 2
     * @param ClassMethod $method
157
     */
158 2
    private function replaceHydrate(ClassMethod $method)
159
    {
160
        $method->params = array(
161
            new Param('data', null, 'array'),
162
            new Param('object'),
163
        );
164
165
        $bodyParts = array();
166 2
        foreach ($this->visiblePropertyMap as $propertyName) {
167
            $bodyParts[] = "if (isset(\$data['" . $propertyName . "'])) {";
168 2
            $bodyParts[] = "    \$object->" . $propertyName . " = \$data['" . $propertyName . "'];";
169 2
            $bodyParts[] = "}";
170
        }
171
        $index = 0;
172 2
        foreach ($this->hiddenPropertyMap as $className => $propertyNames) {
173
            $bodyParts[] = "\$this->hydrateCallbacks[" . ($index++) . "]->__invoke(\$object, \$data);";
174 2
        }
175 1
176
        $bodyParts[] = "return \$object;";
177 1
178 1
        $method->stmts = (new ParserFactory())
0 ignored issues
show
Documentation Bug introduced by
It seems like (new \PhpParser\ParserFa...plode(' ', $bodyParts)) can be null. However, the property $stmts is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
179
            ->create(ParserFactory::ONLY_PHP7)
180
            ->parse('<?php ' . implode("\n", $bodyParts));
181 1
    }
182 1
183
    /**
184
     * @param ClassMethod $method
185
     *
186 2
     * @return void
187 2
     */
188 2
    private function replaceExtract(ClassMethod $method)
189
    {
190
        $method->params = array(new Param('object'));
191 2
192 2
        $bodyParts = array();
193 2
        $bodyParts[] = "\$ret = array();";
194 2
        foreach ($this->visiblePropertyMap as $propertyName) {
195 2
            $bodyParts[] = "\$ret['" . $propertyName . "'] = \$object->" . $propertyName . ";";
196
        }
197
        $index = 0;
198 2
        foreach ($this->hiddenPropertyMap as $className => $propertyNames) {
199
            $bodyParts[] = "\$this->extractCallbacks[" . ($index++) . "]->__invoke(\$object, \$ret);";
200 2
        }
201 2
202
        $bodyParts[] = "return \$ret;";
203
204
        $method->stmts = (new ParserFactory())
0 ignored issues
show
Documentation Bug introduced by
It seems like (new \PhpParser\ParserFa...plode(' ', $bodyParts)) can be null. However, the property $stmts is declared as array. Maybe change the type of the property to array|null or add a type check?

Our type inference engine has found an assignment of a scalar value (like a string, an integer or null) to a property which is an array.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property.

To type hint that a parameter can be either an array or null, you can set a type hint of array and a default value of null. The PHP interpreter will then accept both an array or null for that parameter.

function aContainsB(array $needle = null, array  $haystack) {
    if (!$needle) {
        return false;
    }

    return array_intersect($haystack, $needle) == $haystack;
}

The function can be called with either null or an array for the parameter $needle but will only accept an array as $haystack.

Loading history...
205
            ->create(ParserFactory::ONLY_PHP7)
206
            ->parse('<?php ' . implode("\n", $bodyParts));
207
    }
208
209
    /**
210
     * Finds or creates a class method (and eventually attaches it to the class itself)
211 2
     *
212
     * @param Class_ $class
213 2
     * @param string                    $name  name of the method
214 2
     *
215
     * @return ClassMethod
216 2
     *
217 2
     * @deprecated not needed if we move away from code replacement
218
     */
219
    private function findOrCreateMethod(Class_ $class, string $name) : ClassMethod
220 2
    {
221
        $foundMethods = array_filter(
222 2
            $class->getMethods(),
223 2
            function (ClassMethod $method) use ($name) : bool {
224
                return $name === $method->name;
225
            }
226 2
        );
227
228
        $method = reset($foundMethods);
229
230
        if (!$method) {
231
            $class->stmts[] = $method = new ClassMethod($name);
232
        }
233
234
        return $method;
235
    }
236
}
237