HydratorMethodsVisitor::replaceConstructor()   A
last analyzed

Complexity

Conditions 4
Paths 5

Size

Total Lines 30

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 4

Importance

Changes 0
Metric Value
dl 0
loc 30
ccs 17
cts 17
cp 1
rs 9.44
c 0
b 0
f 0
cc 4
nc 5
nop 1
crap 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GeneratedHydrator\CodeGenerator\Visitor;
6
7
use PhpParser\Node;
8
use PhpParser\Node\Expr\Array_;
9
use PhpParser\Node\Param;
10
use PhpParser\Node\Stmt\Class_;
11
use PhpParser\Node\Stmt\ClassMethod;
12
use PhpParser\Node\Stmt\Property;
13
use PhpParser\Node\Stmt\PropertyProperty;
14
use PhpParser\NodeVisitorAbstract;
15
use PhpParser\ParserFactory;
16
use ReflectionClass;
17
use ReflectionProperty;
18
use function array_filter;
19
use function array_merge;
20
use function array_values;
21
use function implode;
22
use function reset;
23
use function var_export;
24
25
/**
26
 * Replaces methods `__construct`, `hydrate` and `extract` in the classes of the given AST
27
 *
28
 * @todo as per https://github.com/Ocramius/GeneratedHydrator/pull/59, using a visitor for this is ineffective.
29
 * @todo Instead, we can just create a code generator, since we are not modifying code, but creating it.
30
 */
31
class HydratorMethodsVisitor extends NodeVisitorAbstract
32
{
33
    /**
34
     * @var ObjectProperty[]
35
     * @psalm-var list<ObjectProperty>
36
     */
37
    private $visiblePropertyMap = [];
38
39
    /**
40
     * @var array<string, array<int, ObjectProperty>>
41
     * @psalm-var array<string, list<ObjectProperty>>
42
     */
43 2
    private $hiddenPropertyMap = [];
44
45 2
    public function __construct(ReflectionClass $reflectedClass)
46 2
    {
47
        foreach ($this->findAllInstanceProperties($reflectedClass) as $property) {
48 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...
49 2
50
            if ($property->isPrivate() || $property->isProtected()) {
51 2
                $this->hiddenPropertyMap[$className][] = ObjectProperty::fromReflection($property);
52
            } else {
53
                $this->visiblePropertyMap[] = ObjectProperty::fromReflection($property);
54 2
            }
55
        }
56
    }
57
58
    public function leaveNode(Node $node) : ?Class_
59
    {
60
        if (! $node instanceof Class_) {
61 2
            return null;
62
        }
63 2
64
        $node->stmts[] = new Property(Class_::MODIFIER_PRIVATE, [
65
            new PropertyProperty('hydrateCallbacks', new Array_()),
66
            new PropertyProperty('extractCallbacks', new Array_()),
67 2
        ]);
68 2
69 2
        $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...
70
        $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...
71
        $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...
72 2
73 2
        return $node;
74 2
    }
75
76 2
    /**
77
     * Find all class properties recursively using class hierarchy without
78
     * removing name redefinitions
79
     *
80
     * @return ReflectionProperty[]
81
     */
82
    private function findAllInstanceProperties(?ReflectionClass $class = null) : array
83
    {
84
        if (! $class) {
85
            return [];
86
        }
87 2
88
        return array_values(array_merge(
89 2
            $this->findAllInstanceProperties($class->getParentClass() ?: null), // of course PHP is shit.
90 2
            array_values(array_filter(
91
                $class->getProperties(),
92
                static function (ReflectionProperty $property) : bool {
93 2
                    return ! $property->isStatic();
94 2
                }
95 2
            ))
96 2
        ));
97
    }
98 2
99 2
    /**
100
     * @return string[]
101
     *
102
     * @psalm-return list<string>
103
     */
104
    private function generatePropertyHydrateCall(ObjectProperty $property, string $inputArrayName) : array
105
    {
106
        $propertyName = $property->name;
107 2
        $escapedName  = var_export($propertyName, true);
108
109 2
        if ($property->allowsNull && ! $property->hasDefault) {
110
            return ['$object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '] ?? null;'];
111 2
        }
112
113
        return [
114
            'if (isset(' . $inputArrayName . '[' . $escapedName . '])',
115
            '    || $object->' . $propertyName . ' !== null && \\array_key_exists(' . $escapedName . ', ' . $inputArrayName . ')',
116 2
            ') {',
117
            '    $object->' . $propertyName . ' = ' . $inputArrayName . '[' . $escapedName . '];',
118 2
            '}',
119 2
        ];
120 2
    }
121 2
122 2
    private function replaceConstructor(ClassMethod $method) : void
123 2
    {
124
        $method->params = [];
125 2
126
        $bodyParts = [];
127
128 2
        // Create a set of closures that will be called to hydrate the object.
129 2
        // Array of closures in a naturally indexed array, ordered, which will
130 2
        // then be called in order in the hydrate() and extract() methods.
131
        foreach ($this->hiddenPropertyMap as $className => $properties) {
132 2
            // Hydrate closures
133
            $bodyParts[] = '$this->hydrateCallbacks[] = \\Closure::bind(static function ($object, $values) {';
134
            foreach ($properties as $property) {
135 2
                $bodyParts = array_merge($bodyParts, $this->generatePropertyHydrateCall($property, '$values'));
136 2
            }
137 2
            $bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n";
138 2
139
            // Extract closures
140
            $bodyParts[] = '$this->extractCallbacks[] = \\Closure::bind(static function ($object, &$values) {';
141
            foreach ($properties as $property) {
142
                $propertyName = $property->name;
143 2
                $bodyParts[]  = "    \$values['" . $propertyName . "'] = \$object->" . $propertyName . ';';
144
            }
145 2
            $bodyParts[] = '}, null, ' . var_export($className, true) . ');' . "\n";
146 2
        }
147 2
148
        $method->stmts = (new ParserFactory())
149
            ->create(ParserFactory::ONLY_PHP7)
150 2
            ->parse('<?php ' . implode("\n", $bodyParts));
151 2
    }
152 1
153 1
    private function replaceHydrate(ClassMethod $method) : void
154 1
    {
155 1
        $method->params = [
156
            new Param(new Node\Expr\Variable('data'), null, 'array'),
157 2
            new Param(new Node\Expr\Variable('object')),
158 2
        ];
159 2
160
        $bodyParts = [];
161
        foreach ($this->visiblePropertyMap as $property) {
162 2
            $bodyParts = array_merge($bodyParts, $this->generatePropertyHydrateCall($property, '$data'));
163
        }
164 2
        $index = 0;
165 2
        foreach ($this->hiddenPropertyMap as $className => $propertyNames) {
166 2
            $bodyParts[] = '$this->hydrateCallbacks[' . ($index++) . ']->__invoke($object, $data);';
167 2
        }
168
169
        $bodyParts[] = 'return $object;';
170
171
        $method->stmts = (new ParserFactory())
172
            ->create(ParserFactory::ONLY_PHP7)
173
            ->parse('<?php ' . implode("\n", $bodyParts));
174 2
    }
175
176 2
    private function replaceExtract(ClassMethod $method) : void
177
    {
178 2
        $method->params = [new Param(new Node\Expr\Variable('object'))];
179 2
180 2
        $bodyParts   = [];
181 1
        $bodyParts[] = '$ret = array();';
182
        foreach ($this->visiblePropertyMap as $property) {
183 2
            $propertyName = $property->name;
184 2
            $bodyParts[]  = "\$ret['" . $propertyName . "'] = \$object->" . $propertyName . ';';
185 2
        }
186
        $index = 0;
187
        foreach ($this->hiddenPropertyMap as $className => $property) {
188 2
            $bodyParts[] = '$this->extractCallbacks[' . ($index++) . ']->__invoke($object, $ret);';
189
        }
190 2
191 2
        $bodyParts[] = 'return $ret;';
192 2
193 2
        $method->stmts = (new ParserFactory())
194
            ->create(ParserFactory::ONLY_PHP7)
195
            ->parse('<?php ' . implode("\n", $bodyParts));
196
    }
197
198
    /**
199
     * Finds or creates a class method (and eventually attaches it to the class itself)
200
     *
201
     * @deprecated not needed if we move away from code replacement
202
     */
203
    private function findOrCreateMethod(Class_ $class, string $name) : ClassMethod
204
    {
205 2
        $foundMethods = array_filter(
206
            $class->getMethods(),
207 2
            static function (ClassMethod $method) use ($name) : bool {
208 2
                return $name === $method->name;
209
            }
210 2
        );
211 2
212
        $method = reset($foundMethods);
213
214 2
        if (! $method) {
215
            $class->stmts[] = $method = new ClassMethod($name);
216 2
        }
217 2
218
        return $method;
219
    }
220
}
221