HydratorFunctionalTest::testHydratingNull()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 10
rs 9.9332
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 Laminas\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
     * @return mixed[]
131
     */
132
    public function getHydratorClasses() : array
133
    {
134
        return [
135
            [new stdClass()],
136
            [new EmptyClass()],
137
            [new HydratedObject()],
138
            [new BaseClass()],
139
            [new ClassWithPublicProperties()],
140
            [new ClassWithProtectedProperties()],
141
            [new ClassWithPrivateProperties()],
142
            [new ClassWithPrivatePropertiesAndParent()],
143
            [new ClassWithPrivatePropertiesAndParents()],
144
            [new ClassWithMixedProperties()],
145
            [new ClassWithStaticProperties()],
146
        ];
147
    }
148
149
    /**
150
     * Recursively populate the $initialData and $newData array browsing the
151
     * full class hierarchy tree
152
     *
153
     * Private properties from parent class that are hidden by children will be
154
     * dropped from the populated arrays
155
     *
156
     * @param mixed[] $initialData
157
     * @param mixed[] $newData
158
     */
159
    private function recursiveFindInitialData(
160
        ReflectionClass $class,
161
        object $instance,
162
        array &$initialData,
163
        array &$newData
164
    ) : void {
165
        $parentClass = $class->getParentClass();
166
        if ($parentClass) {
167
            $this->recursiveFindInitialData($parentClass, $instance, $initialData, $newData);
168
        }
169
170
        foreach ($class->getProperties() as $property) {
171
            if ($property->isStatic()) {
172
                continue;
173
            }
174
175
            $propertyName = $property->getName();
176
177
            $property->setAccessible(true);
178
            $initialData[$propertyName] = $property->getValue($instance);
179
            $newData[$propertyName]     = $property->getName() . '__new__value';
180
        }
181
    }
182
183
    /**
184
     * Recursively populate the $inspectedData array browsing the full class
185
     * hierarchy tree
186
     *
187
     * Private properties from parent class that are hidden by children will be
188
     * dropped from the populated arrays
189
     *
190
     * @param mixed[] $inspectionData
191
     */
192
    private function recursiveFindInspectionData(
193
        ReflectionClass $class,
194
        object $instance,
195
        array &$inspectionData
196
    ) : void {
197
        $parentClass = $class->getParentClass();
198
        if ($parentClass) {
199
            $this->recursiveFindInspectionData($parentClass, $instance, $inspectionData);
200
        }
201
202
        foreach ($class->getProperties() as $property) {
203
            if ($property->isStatic()) {
204
                continue;
205
            }
206
207
            $propertyName = $property->getName();
208
209
            $property->setAccessible(true);
210
            $inspectionData[$propertyName] = $property->getValue($instance);
211
        }
212
    }
213
214
    /**
215
     * Generates a hydrator for the given class name, and retrieves its class name
216
     */
217
    private function generateHydrator(object $instance) : HydratorInterface
218
    {
219
        $parentClassName    = get_class($instance);
220
        $generatedClassName = __NAMESPACE__ . '\\' . UniqueIdentifierGenerator::getIdentifier('Foo');
221
        $config             = new Configuration($parentClassName);
222
        /** @var ClassNameInflectorInterface|MockObject $inflector*/
223
        $inflector = $this->createMock(ClassNameInflectorInterface::class);
224
225
        $inflector
226
            ->expects(self::any())
227
            ->method('getGeneratedClassName')
228
            ->with($parentClassName)
229
            ->will(self::returnValue($generatedClassName));
230
        $inflector
231
            ->expects(self::any())
232
            ->method('getUserClassName')
233
            ->with($parentClassName)
234
            ->will(self::returnValue($parentClassName));
235
236
        $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...
237
        $config->setGeneratorStrategy(new EvaluatingGeneratorStrategy());
238
239
        $generatedClass = $config->createFactory()->getHydratorClass();
240
241
        return new $generatedClass();
242
    }
243
}
244