Completed
Pull Request — master (#133)
by
unknown
21:29
created

AbstractHydratorMethodsVisitor::leaveNode()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

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