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

AbstractHydratorFunctionalTest   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 321
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 18

Importance

Changes 0
Metric Value
wmc 16
lcom 1
cbo 18
dl 0
loc 321
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A testHydrator() 0 29 1
A recursiveFindInitialData() 0 23 4
A generateHydrator() 0 27 1
A recursiveFindInspectionData() 0 21 4
A testHydratingNull() 0 10 1
A testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError() 0 17 1
A testHydratorWillSetAllTypedProperties() 0 19 1
B testHydratorWillHydrateWithStrategies() 0 56 1
B testHydratorWillHydratePrivatePropertiesWithStrategies() 0 56 1
A getHydratorClasses() 0 16 1
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\ClassGenerator\AbstractHydratorGenerator;
11
use GeneratedHydrator\Configuration;
12
use GeneratedHydrator\Strategy\RecursiveHydrationStrategy;
13
use GeneratedHydratorTestAsset\BaseClass;
14
use GeneratedHydratorTestAsset\ClassWithMixedProperties;
15
use GeneratedHydratorTestAsset\ClassWithObjectProperty;
16
use GeneratedHydratorTestAsset\ClassWithPrivateObjectProperty;
17
use GeneratedHydratorTestAsset\ClassWithPrivateProperties;
18
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParent;
19
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParents;
20
use GeneratedHydratorTestAsset\ClassWithProtectedProperties;
21
use GeneratedHydratorTestAsset\ClassWithPublicProperties;
22
use GeneratedHydratorTestAsset\ClassWithStaticProperties;
23
use GeneratedHydratorTestAsset\ClassWithTypedProperties;
24
use GeneratedHydratorTestAsset\EmptyClass;
25
use GeneratedHydratorTestAsset\HydratedObject;
26
use PHPUnit\Framework\MockObject\MockObject;
27
use PHPUnit\Framework\TestCase;
28
use ReflectionClass;
29
use stdClass;
30
use Zend\Hydrator\AbstractHydrator;
31
use Zend\Hydrator\HydratorInterface;
32
use Zend\Hydrator\Strategy\ClosureStrategy;
33
use function get_class;
34
use function ksort;
35
36
/**
37
 * Tests for {@see \GeneratedHydrator\ClassGenerator\HydratorGenerator} produced objects
38
 * @group Functional
39
 */
40
class AbstractHydratorFunctionalTest extends TestCase
41
{
42
    /**
43
     * @dataProvider getHydratorClasses
44
     */
45
    public function testHydrator(object $instance): void
46
    {
47
        $reflection = new ReflectionClass($instance);
48
        $initialData = [];
49
        $newData = [];
50
51
        $this->recursiveFindInitialData($reflection, $instance, $initialData, $newData);
52
53
        $generatedClass = $this->generateHydrator($instance);
54
55
        // Hydration and extraction don't guarantee ordering.
56
        ksort($initialData);
57
        ksort($newData);
58
        $extracted = $generatedClass->extract($instance);
59
        ksort($extracted);
60
61
        self::assertSame($initialData, $extracted);
62
        self::assertSame($instance, $generatedClass->hydrate($newData, $instance));
63
64
        // Same as upper applies
65
        $inspectionData = [];
66
        $this->recursiveFindInspectionData($reflection, $instance, $inspectionData);
67
        ksort($inspectionData);
68
        $extracted = $generatedClass->extract($instance);
69
        ksort($extracted);
70
71
        self::assertSame($inspectionData, $newData);
72
        self::assertSame($inspectionData, $extracted);
73
    }
74
75
    /**
76
     * Recursively populate the $initialData and $newData array browsing the
77
     * full class hierarchy tree
78
     * Private properties from parent class that are hidden by children will be
79
     * dropped from the populated arrays
80
     *
81
     * @param mixed[] $initialData
82
     * @param mixed[] $newData
83
     */
84
    private function recursiveFindInitialData(
85
        ReflectionClass $class,
86
        object $instance,
87
        array &$initialData,
88
        array &$newData
89
    ): void {
90
        $parentClass = $class->getParentClass();
91
        if ($parentClass) {
92
            $this->recursiveFindInitialData($parentClass, $instance, $initialData, $newData);
93
        }
94
95
        foreach ($class->getProperties() as $property) {
96
            if ($property->isStatic()) {
97
                continue;
98
            }
99
100
            $propertyName = $property->getName();
101
102
            $property->setAccessible(true);
103
            $initialData[$propertyName] = $property->getValue($instance);
104
            $newData[$propertyName] = $property->getName() . '__new__value';
105
        }
106
    }
107
108
    /**
109
     * Generates a hydrator for the given class name, and retrieves its class name
110
     */
111
    private function generateHydrator(object $instance): HydratorInterface
112
    {
113
        $parentClassName = get_class($instance);
114
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
115
        $config = new Configuration($parentClassName);
116
        $config->setHydratorGenerator(new AbstractHydratorGenerator());
117
        /** @var ClassNameInflectorInterface|MockObject $inflector */
118
        $inflector = $this->createMock(ClassNameInflectorInterface::class);
119
120
        $inflector
0 ignored issues
show
Bug introduced by
The method expects does only exist in PHPUnit\Framework\MockObject\MockObject, but not in CodeGenerationUtils\Infl...sNameInflectorInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
121
            ->expects(self::any())
122
            ->method('getGeneratedClassName')
123
            ->with($parentClassName)
124
            ->will(self::returnValue($generatedClassName));
125
        $inflector
126
            ->expects(self::any())
127
            ->method('getUserClassName')
128
            ->with($parentClassName)
129
            ->will(self::returnValue($parentClassName));
130
131
        $config->setClassNameInflector($inflector);
0 ignored issues
show
Bug introduced by
It seems like $inflector defined by $this->createMock(\CodeG...lectorInterface::class) on line 118 can also be of type object<PHPUnit\Framework\MockObject\MockObject>; however, GeneratedHydrator\Config...setClassNameInflector() does only seem to accept object<CodeGenerationUti...NameInflectorInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
132
        $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
133
134
        $generatedClass = $config->createFactory()->getHydratorClass();
135
136
        return new $generatedClass();
137
    }
138
139
    /**
140
     * Recursively populate the $inspectedData array browsing the full class
141
     * hierarchy tree
142
     * Private properties from parent class that are hidden by children will be
143
     * dropped from the populated arrays
144
     *
145
     * @param mixed[] $inspectionData
146
     */
147
    private function recursiveFindInspectionData(
148
        ReflectionClass $class,
149
        object $instance,
150
        array &$inspectionData
151
    ): void {
152
        $parentClass = $class->getParentClass();
153
        if ($parentClass) {
154
            $this->recursiveFindInspectionData($parentClass, $instance, $inspectionData);
155
        }
156
157
        foreach ($class->getProperties() as $property) {
158
            if ($property->isStatic()) {
159
                continue;
160
            }
161
162
            $propertyName = $property->getName();
163
164
            $property->setAccessible(true);
165
            $inspectionData[$propertyName] = $property->getValue($instance);
166
        }
167
    }
168
169
    public function testHydratingNull(): void
170
    {
171
        $instance = new ClassWithPrivateProperties();
172
173
        self::assertSame('property0', $instance->getProperty0());
174
175
        $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...
176
177
        self::assertNull($instance->getProperty0());
178
    }
179
180
    /**
181
     * Ensures that the hydrator will not attempt to read unitialized PHP >= 7.4
182
     * typed property, which would cause "Uncaught Error: Typed property Foo::$a
183
     * must not be accessed before initialization" PHP engine errors.
184
     * @requires PHP >= 7.4
185
     */
186
    public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError(): void
187
    {
188
        $instance = new ClassWithTypedProperties();
189
        $hydrator = $this->generateHydrator($instance);
190
191
        $hydrator->hydrate(['property2' => 3], $instance);
192
193
        self::assertSame([
194
            'property0' => 1, // 'property0' has a default value, it should keep it.
195
            'property1' => 2, // 'property1' has a default value, it should keep it.
196
            'property2' => 3,
197
            'property3' => null, // 'property3' is not required, it should remain null.
198
            'property4' => null, // 'property4' default value is null, it should remain null.
199
            'untyped0' => null, // 'untyped0' is null by default
200
            'untyped1' => null, // 'untyped1' is null by default
201
        ], $hydrator->extract($instance));
202
    }
203
204
    /**
205
     * @requires PHP >= 7.4
206
     */
207
    public function testHydratorWillSetAllTypedProperties(): void
208
    {
209
        $instance = new ClassWithTypedProperties();
210
        $hydrator = $this->generateHydrator($instance);
211
212
        $reference = [
213
            'property0' => 11,
214
            'property1' => null, // Ensure explicit set null works as expected.
215
            'property2' => 13,
216
            'property3' => null, // Different use case (unrequired value with no default value).
217
            'property4' => 19,
218
            'untyped0' => null, // 'untyped0' is null by default
219
            'untyped1' => null, // 'untyped1' is null by default
220
        ];
221
222
        $hydrator->hydrate($reference, $instance);
223
224
        self::assertSame($reference, $hydrator->extract($instance));
225
    }
226
227
    public function testHydratorWillHydrateWithStrategies(): void
228
    {
229
        $instance = new ClassWithObjectProperty();
230
231
        $nestedHydrator = $this->generateHydrator(new ClassWithPublicProperties());
0 ignored issues
show
Documentation introduced by
new \GeneratedHydratorTe...sWithPublicProperties() is of type object<GeneratedHydrator...ssWithPublicProperties>, 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...
232
233
        /** @var AbstractHydrator $hydrator */
234
        $hydrator = $this->generateHydrator($instance);
235
        $hydrator->addStrategy('publicProperties', new RecursiveHydrationStrategy($nestedHydrator, ClassWithPublicProperties::class));
236
        $hydrator->addStrategy('publicPropertiesCollection', new RecursiveHydrationStrategy($nestedHydrator, ClassWithPublicProperties::class, true));
237
238
        $reference = [
239
            'publicProperties' => [
240
                'property0' => 'Prop0',
241
                'property1' => 'Prop1',
242
                'property2' => 'Prop2',
243
                'property3' => 'Prop3',
244
                'property4' => 'Prop4',
245
                'property5' => 'Prop5',
246
                'property6' => 'Prop6',
247
                'property7' => 'Prop7',
248
                'property8' => 'Prop8',
249
                'property9' => 'Prop9',
250
            ],
251
            'publicPropertiesCollection' => [
252
                [
253
                    'property0' => 'Prop10',
254
                    'property1' => 'Prop11',
255
                    'property2' => 'Prop12',
256
                    'property3' => 'Prop13',
257
                    'property4' => 'Prop14',
258
                    'property5' => 'Prop15',
259
                    'property6' => 'Prop16',
260
                    'property7' => 'Prop17',
261
                    'property8' => 'Prop18',
262
                    'property9' => 'Prop19',
263
                ],
264
                [
265
                    'property0' => 'Prop20',
266
                    'property1' => 'Prop21',
267
                    'property2' => 'Prop22',
268
                    'property3' => 'Prop23',
269
                    'property4' => 'Prop24',
270
                    'property5' => 'Prop25',
271
                    'property6' => 'Prop26',
272
                    'property7' => 'Prop27',
273
                    'property8' => 'Prop28',
274
                    'property9' => 'Prop29',
275
                ]
276
            ],
277
        ];
278
279
        $hydrator->hydrate($reference, $instance);
280
281
        self::assertSame($reference, $hydrator->extract($instance));
282
    }
283
284
    public function testHydratorWillHydratePrivatePropertiesWithStrategies(): void
285
    {
286
        $instance = new ClassWithPrivateObjectProperty();
287
288
        $nestedHydrator = $this->generateHydrator(new ClassWithPrivateProperties());
0 ignored issues
show
Documentation introduced by
new \GeneratedHydratorTe...WithPrivateProperties() 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...
289
290
        /** @var AbstractHydrator $hydrator */
291
        $hydrator = $this->generateHydrator($instance);
292
        $hydrator->addStrategy('privateProperties', new RecursiveHydrationStrategy($nestedHydrator, ClassWithPrivateProperties::class));
293
        $hydrator->addStrategy('privatePropertiesCollection', new RecursiveHydrationStrategy($nestedHydrator, ClassWithPrivateProperties::class, true));
294
295
        $reference = [
296
            'privateProperties' => [
297
                'property0' => 'Prop0',
298
                'property1' => 'Prop1',
299
                'property2' => 'Prop2',
300
                'property3' => 'Prop3',
301
                'property4' => 'Prop4',
302
                'property5' => 'Prop5',
303
                'property6' => 'Prop6',
304
                'property7' => 'Prop7',
305
                'property8' => 'Prop8',
306
                'property9' => 'Prop9',
307
            ],
308
            'privatePropertiesCollection' => [
309
                [
310
                    'property0' => 'Prop10',
311
                    'property1' => 'Prop11',
312
                    'property2' => 'Prop12',
313
                    'property3' => 'Prop13',
314
                    'property4' => 'Prop14',
315
                    'property5' => 'Prop15',
316
                    'property6' => 'Prop16',
317
                    'property7' => 'Prop17',
318
                    'property8' => 'Prop18',
319
                    'property9' => 'Prop19',
320
                ],
321
                [
322
                    'property0' => 'Prop20',
323
                    'property1' => 'Prop21',
324
                    'property2' => 'Prop22',
325
                    'property3' => 'Prop23',
326
                    'property4' => 'Prop24',
327
                    'property5' => 'Prop25',
328
                    'property6' => 'Prop26',
329
                    'property7' => 'Prop27',
330
                    'property8' => 'Prop28',
331
                    'property9' => 'Prop29',
332
                ]
333
            ],
334
        ];
335
336
        $hydrator->hydrate($reference, $instance);
337
338
        self::assertSame($reference, $hydrator->extract($instance));
339
    }
340
341
    /**
342
     * @return mixed[]
343
     */
344
    public function getHydratorClasses(): array
345
    {
346
        return [
347
            [new stdClass()],
348
            [new EmptyClass()],
349
            [new HydratedObject()],
350
            [new BaseClass()],
351
            [new ClassWithPublicProperties()],
352
            [new ClassWithProtectedProperties()],
353
            [new ClassWithPrivateProperties()],
354
            [new ClassWithPrivatePropertiesAndParent()],
355
            [new ClassWithPrivatePropertiesAndParents()],
356
            [new ClassWithMixedProperties()],
357
            [new ClassWithStaticProperties()],
358
        ];
359
    }
360
}
361