Completed
Pull Request — master (#142)
by
unknown
21:09
created

testHydratorWithMappedFromAnnotation()   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\ClassWithMappedProperties;
13
use GeneratedHydratorTestAsset\ClassWithMixedProperties;
14
use GeneratedHydratorTestAsset\ClassWithPrivateProperties;
15
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParent;
16
use GeneratedHydratorTestAsset\ClassWithPrivatePropertiesAndParents;
17
use GeneratedHydratorTestAsset\ClassWithProtectedProperties;
18
use GeneratedHydratorTestAsset\ClassWithPublicProperties;
19
use GeneratedHydratorTestAsset\ClassWithStaticProperties;
20
use GeneratedHydratorTestAsset\ClassWithTypedProperties;
21
use GeneratedHydratorTestAsset\EmptyClass;
22
use GeneratedHydratorTestAsset\HydratedObject;
23
use PHPUnit\Framework\MockObject\MockObject;
24
use PHPUnit\Framework\TestCase;
25
use ReflectionClass;
26
use stdClass;
27
use Laminas\Hydrator\HydratorInterface;
28
use function get_class;
29
use function ksort;
30
31
/**
32
 * Tests for {@see \GeneratedHydrator\ClassGenerator\HydratorGenerator} produced objects
33
 *
34
 * @group Functional
35
 */
36
class HydratorFunctionalTest extends TestCase
37
{
38
    /**
39
     * @dataProvider getHydratorClasses
40
     */
41
    public function testHydrator(object $instance) : void
42
    {
43
        $reflection  = new ReflectionClass($instance);
44
        $initialData = [];
45
        $newData     = [];
46
47
        $this->recursiveFindInitialData($reflection, $instance, $initialData, $newData);
48
49
        $generatedClass = $this->generateHydrator($instance);
50
51
        // Hydration and extraction don't guarantee ordering.
52
        ksort($initialData);
53
        ksort($newData);
54
        $extracted = $generatedClass->extract($instance);
55
        ksort($extracted);
56
57
        self::assertSame($initialData, $extracted);
58
        self::assertSame($instance, $generatedClass->hydrate($newData, $instance));
59
60
        // Same as upper applies
61
        $inspectionData = [];
62
        $this->recursiveFindInspectionData($reflection, $instance, $inspectionData);
63
        ksort($inspectionData);
64
        $extracted = $generatedClass->extract($instance);
65
        ksort($extracted);
66
67
        self::assertSame($inspectionData, $newData);
68
        self::assertSame($inspectionData, $extracted);
69
    }
70
71
    public function testHydratingNull() : void
72
    {
73
        $instance = new ClassWithPrivateProperties();
74
75
        self::assertSame('property0', $instance->getProperty0());
76
77
        $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...
78
79
        self::assertNull($instance->getProperty0());
80
    }
81
82
    /**
83
     * Ensures that the hydrator will not attempt to read unitialized PHP >= 7.4
84
     * typed property, which would cause "Uncaught Error: Typed property Foo::$a
85
     * must not be accessed before initialization" PHP engine errors.
86
     *
87
     * @requires PHP >= 7.4
88
     */
89
    public function testHydratorWillNotRaisedUnitiliazedTypedPropertyAccessError() : void
90
    {
91
        $instance = new ClassWithTypedProperties();
92
        $hydrator = $this->generateHydrator($instance);
93
94
        $hydrator->hydrate(['property2' => 3], $instance);
95
96
        self::assertSame([
97
            'property0' => 1, // 'property0' has a default value, it should keep it.
98
            'property1' => 2, // 'property1' has a default value, it should keep it.
99
            'property2' => 3,
100
            'property3' => null, // 'property3' is not required, it should remain null.
101
            'property4' => null, // 'property4' default value is null, it should remain null.
102
            'untyped0' => null, // 'untyped0' is null by default
103
            'untyped1' => null, // 'untyped1' is null by default,
104
            'property5' => 'test'
105
        ], $hydrator->extract($instance));
106
    }
107
108
    public function testHydratorWithMappedFromAnnotation() : void
109
    {
110
        $instance = new ClassWithMappedProperties();
111
        $hydrator = $this->generateHydrator($instance);
112
113
        $hydratedInstance = $hydrator->hydrate([
114
            'test0' => 9,
115
            'test1' => 8,
116
            'test2' => 7,
117
            'test3' => 6,
118
        ], $instance);
119
120
        self::assertSame(9, $hydratedInstance->property0);
121
        self::assertSame(8, $hydratedInstance->property1);
122
        self::assertSame(7, $hydratedInstance->property2);
123
        self::assertSame(6, $hydratedInstance->property3);
124
    }
125
126
    /**
127
     * @requires PHP >= 7.4
128
     */
129
    public function testHydratorWillSetAllTypedProperties() : void
130
    {
131
        $instance = new ClassWithTypedProperties();
132
        $hydrator = $this->generateHydrator($instance);
133
134
        $reference = [
135
            'property0' => 11,
136
            'property1' => null, // Ensure explicit set null works as expected.
137
            'property2' => 13,
138
            'property3' => null, // Different use case (unrequired value with no default value).
139
            'property4' => 19,
140
            'untyped0' => null, // 'untyped0' is null by default
141
            'untyped1' => null, // 'untyped1' is null by default
142
            'property5' => 'test'
143
        ];
144
145
        $hydrator->hydrate($reference, $instance);
146
147
        self::assertSame($reference, $hydrator->extract($instance));
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