Completed
Pull Request — master (#124)
by Anton
23:06
created

testExtractSkipsNonSetValues()   A

Complexity

Conditions 1
Paths 1

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 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace GeneratedHydratorTest\Functional;
6
7
use CodeGenerationUtils\GeneratorStrategy\EvaluatingGeneratorStrategy;
8
use CodeGenerationUtils\Inflector\ClassNameInflectorInterface;
9
use CodeGenerationUtils\Inflector\Util\UniqueIdentifierGenerator;
10
use GeneratedHydrator\Configuration;
11
use GeneratedHydratorTestAsset\BaseClass;
12
use GeneratedHydratorTestAsset\ClassWithMixedProperties;
13
use GeneratedHydratorTestAsset\ClassWithPrivateProperties;
14
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParent;
15
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParents;
16
use GeneratedHydratorTestAsset\ClassWithProtectedProperties;
17
use GeneratedHydratorTestAsset\ClassWithPublicProperties;
18
use GeneratedHydratorTestAsset\ClassWithStaticProperties;
19
use GeneratedHydratorTestAsset\ClassWithTypedProperties;
20
use GeneratedHydratorTestAsset\EmptyClass;
21
use GeneratedHydratorTestAsset\HydratedObject;
22
use PHPUnit\Framework\MockObject\MockObject;
23
use PHPUnit\Framework\TestCase;
24
use ReflectionClass;
25
use stdClass;
26
use Zend\Hydrator\HydratorInterface;
27
use function get_class;
28
use function ksort;
29
30
/**
31
 * Tests for {@see \GeneratedHydrator\ClassGenerator\HydratorGenerator} produced objects
32
 *
33
 * @group Functional
34
 */
35
class HydratorFunctionalTest extends TestCase
36
{
37
    /**
38
     * @dataProvider getHydratorClasses
39
     */
40
    public function testHydrator(object $instance) : void
41
    {
42
        $reflection  = new ReflectionClass($instance);
43
        $initialData = [];
44
        $newData     = [];
45
46
        $this->recursiveFindInitialData($reflection, $instance, $initialData, $newData);
47
48
        $generatedClass = $this->generateHydrator($instance);
49
50
        // Hydration and extraction don't guarantee ordering.
51
        ksort($initialData);
52
        ksort($newData);
53
        $extracted = $generatedClass->extract($instance);
54
        ksort($extracted);
55
56
        self::assertSame($initialData, $extracted);
57
        self::assertSame($instance, $generatedClass->hydrate($newData, $instance));
58
59
        // Same as upper applies
60
        $inspectionData = [];
61
        $this->recursiveFindInspectionData($reflection, $instance, $inspectionData);
62
        ksort($inspectionData);
63
        $extracted = $generatedClass->extract($instance);
64
        ksort($extracted);
65
66
        self::assertSame($inspectionData, $newData);
67
        self::assertSame($inspectionData, $extracted);
68
    }
69
70
    public function testHydratingNull() : void
71
    {
72
        $instance = new ClassWithPrivateProperties();
73
74
        self::assertSame('property0', $instance->getProperty0());
75
76
        $this->generateHydrator($instance)->hydrate(['property0' => null], $instance);
0 ignored issues
show
Documentation introduced by
$instance is of type object<GeneratedHydrator...sWithPrivateProperties>, but the function expects a object<GeneratedHydratorTest\Functional\object>.

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...
77
78
        self::assertNull($instance->getProperty0());
79
    }
80
81
    /**
82
     * Ensures that the hydrator will not attempt to read unitialized PHP >= 7.4
83
     * typed property, which would cause "Uncaught Error: Typed property Foo::$a
84
     * must not be accessed before initialization" PHP engine errors.
85
     *
86
     * @requires PHP >= 7.4
87
     */
88
    public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError() : void
89
    {
90
        $instance = new ClassWithTypedProperties();
91
        $hydrator = $this->generateHydrator($instance);
92
93
        $hydrator->hydrate(['property2' => 3], $instance);
94
95
        self::assertSame([
96
            'property0' => 1, // 'property0' has a default value, it should keep it.
97
            'property1' => 2, // 'property1' has a default value, it should keep it.
98
            'property2' => 3,
99
            'property3' => null, // 'property3' is not required, it should remain null.
100
            'property4' => null, // 'property4' default value is null, it should remain null.
101
            'untyped0' => null, // 'untyped0' is null by default
102
            'untyped1' => null, // 'untyped1' is null by default
103
        ], $hydrator->extract($instance));
104
    }
105
106
    /**
107
     * @requires PHP >= 7.4
108
     */
109
    public function testHydratorWillSetAllTypedProperties() : void
110
    {
111
        $instance = new ClassWithTypedProperties();
112
        $hydrator = $this->generateHydrator($instance);
113
114
        $reference = [
115
            'property0' => 11,
116
            'property1' => null, // Ensure explicit set null works as expected.
117
            'property2' => 13,
118
            'property3' => null, // Different use case (unrequired value with no default value).
119
            'property4' => 19,
120
            'untyped0' => null, // 'untyped0' is null by default
121
            'untyped1' => null, // 'untyped1' is null by default
122
        ];
123
124
        $hydrator->hydrate($reference, $instance);
125
126
        self::assertSame($reference, $hydrator->extract($instance));
127
    }
128
129
    /**
130
     * @requires PHP >= 7.4
131
     */
132
    public function testExtractSkipsNonSetValues() : void
133
    {
134
        $instance = new ClassWithTypedProperties();
135
        $hydrator = $this->generateHydrator($instance);
136
137
        $reference =  $hydrator->extract($instance);
138
139
        self::assertSame([
140
            'property0' => 1,
141
            'property1' => 2,
142
            // $property2 skipped as it does not exists
143
            // $property3 skipped as it does not exists
144
            'property4' => null,
145
            'untyped0' => null, // 'untyped0' is null by default
146
            'untyped1' => null, // 'untyped1' is null by default
147
        ], $reference);
148
    }
149
150
    /**
151
     * @return mixed[]
152
     */
153
    public function getHydratorClasses() : array
154
    {
155
        return [
156
            [new stdClass()],
157
            [new EmptyClass()],
158
            [new HydratedObject()],
159
            [new BaseClass()],
160
            [new ClassWithPublicProperties()],
161
            [new ClassWithProtectedProperties()],
162
            [new ClassWithPrivateProperties()],
163
            [new ClassWithPrivatePropertiesAndParent()],
164
            [new ClassWithPrivatePropertiesAndParents()],
165
            [new ClassWithMixedProperties()],
166
            [new ClassWithStaticProperties()],
167
        ];
168
    }
169
170
    /**
171
     * Recursively populate the $initialData and $newData array browsing the
172
     * full class hierarchy tree
173
     *
174
     * Private properties from parent class that are hidden by children will be
175
     * dropped from the populated arrays
176
     *
177
     * @param mixed[] $initialData
178
     * @param mixed[] $newData
179
     */
180
    private function recursiveFindInitialData(
181
        ReflectionClass $class,
182
        object $instance,
183
        array &$initialData,
184
        array &$newData
185
    ) : void {
186
        $parentClass = $class->getParentClass();
187
        if ($parentClass) {
188
            $this->recursiveFindInitialData($parentClass, $instance, $initialData, $newData);
189
        }
190
191
        foreach ($class->getProperties() as $property) {
192
            if ($property->isStatic()) {
193
                continue;
194
            }
195
196
            $propertyName = $property->getName();
197
198
            $property->setAccessible(true);
199
            $initialData[$propertyName] = $property->getValue($instance);
200
            $newData[$propertyName]     = $property->getName() . '__new__value';
201
        }
202
    }
203
204
    /**
205
     * Recursively populate the $inspectedData array browsing the full class
206
     * hierarchy tree
207
     *
208
     * Private properties from parent class that are hidden by children will be
209
     * dropped from the populated arrays
210
     *
211
     * @param mixed[] $inspectionData
212
     */
213
    private function recursiveFindInspectionData(
214
        ReflectionClass $class,
215
        object $instance,
216
        array &$inspectionData
217
    ) : void {
218
        $parentClass = $class->getParentClass();
219
        if ($parentClass) {
220
            $this->recursiveFindInspectionData($parentClass, $instance, $inspectionData);
221
        }
222
223
        foreach ($class->getProperties() as $property) {
224
            if ($property->isStatic()) {
225
                continue;
226
            }
227
228
            $propertyName = $property->getName();
229
230
            $property->setAccessible(true);
231
            $inspectionData[$propertyName] = $property->getValue($instance);
232
        }
233
    }
234
235
    /**
236
     * Generates a hydrator for the given class name, and retrieves its class name
237
     */
238
    private function generateHydrator(object $instance) : HydratorInterface
239
    {
240
        $parentClassName    = get_class($instance);
241
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
242
        $config             = new Configuration($parentClassName);
243
        /** @var ClassNameInflectorInterface|MockObject $inflector*/
244
        $inflector = $this->createMock(ClassNameInflectorInterface::class);
245
246
        $inflector
247
            ->expects(self::any())
248
            ->method('getGeneratedClassName')
249
            ->with($parentClassName)
250
            ->will(self::returnValue($generatedClassName));
251
        $inflector
252
            ->expects(self::any())
253
            ->method('getUserClassName')
254
            ->with($parentClassName)
255
            ->will(self::returnValue($parentClassName));
256
257
        $config->setClassNameInflector($inflector);
0 ignored issues
show
Documentation introduced by
$inflector is of type object<PHPUnit\Framework\MockObject\MockObject>, but the function expects a object<CodeGenerationUti...NameInflectorInterface>.

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...
258
        $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
259
260
        $generatedClass = $config->createFactory()->getHydratorClass();
261
262
        return new $generatedClass();
263
    }
264
}
265