Completed
Pull Request — 3.x (#5937)
by Peter
03:39
created

DataTransformerResolverTest::setUp()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 * This file is part of the Sonata Project package.
7
 *
8
 * (c) Thomas Rabaix <[email protected]>
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Sonata\AdminBundle\Tests\Form;
15
16
use PHPUnit\Framework\TestCase;
17
use Sonata\AdminBundle\Admin\FieldDescriptionInterface;
18
use Sonata\AdminBundle\Form\DataTransformer\BooleanToStringTransformer;
19
use Sonata\AdminBundle\Form\DataTransformer\ModelToIdTransformer;
20
use Sonata\AdminBundle\Form\DataTransformerResolver;
21
use Sonata\AdminBundle\Model\ModelManagerInterface;
22
use Symfony\Component\Form\CallbackTransformer;
23
use Symfony\Component\Form\DataTransformerInterface;
24
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
25
26
/**
27
 * @author Peter Gribanov <[email protected]>
28
 */
29
final class DataTransformerResolverTest extends TestCase
30
{
31
    /**
32
     * @var DataTransformerResolver
33
     */
34
    private $resolver;
35
36
    /**
37
     * @var FieldDescriptionInterface
38
     */
39
    private $fieldDescription;
40
41
    /**
42
     * @var FieldDescriptionInterface
43
     */
44
    private $modelManager;
45
46
    protected function setUp(): void
47
    {
48
        $this->fieldDescription = $this->prophesize(FieldDescriptionInterface::class);
49
        $this->modelManager = $this->prophesize(ModelManagerInterface::class);
50
        $this->resolver = new DataTransformerResolver();
51
    }
52
53
    public function testFailedResolve(): void
54
    {
55
        $this->assertNull($this->resolve());
56
    }
57
58
    public function provideFieldTypes(): array
59
    {
60
        return [
61
            ['foo'],
62
            // override predefined transformers
63
            ['date'],
64
            ['boolean'],
65
            ['choice'],
66
        ];
67
    }
68
69
    /**
70
     * @dataProvider provideFieldTypes
71
     */
72
    public function testResolveCustomDataTransformer(string $fieldType): void
73
    {
74
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
75
            return (string) (int) $value;
76
        }, static function ($value): bool {
77
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
78
        });
79
        $this->fieldDescription->getOption('data_transformer')->willReturn($customDataTransformer);
80
        $this->fieldDescription->getType()->willReturn($fieldType);
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
81
82
        $dataTransformer = $this->resolve();
83
84
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
85
        $this->assertSame($customDataTransformer, $dataTransformer);
86
    }
87
88
    public function getTimeZones(): iterable
89
    {
90
        $default = new \DateTimeZone(date_default_timezone_get());
91
        $custom = new \DateTimeZone('Europe/Rome');
92
93
        return [
94
            'empty timezone' => [null, $default],
95
            'disabled timezone' => [false, $default],
96
            'default timezone by name' => [$default->getName(), $default],
97
            'default timezone by object' => [$default, $default],
98
            'custom timezone by name' => [$custom->getName(), $custom],
99
            'custom timezone by object' => [$custom, $custom],
100
        ];
101
    }
102
103
    /**
104
     * @dataProvider getTimeZones
105
     */
106
    public function testResolveDateDataTransformer($timezone, \DateTimeZone $expectedTimezone): void
107
    {
108
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
109
        $this->fieldDescription->getOption('timezone')->willReturn($timezone);
110
        $this->fieldDescription->getType()->willReturn('date');
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
111
112
        $dataTransformer = $this->resolve();
113
114
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
115
116
        $value = '2020-12-12';
117
        $defaultTimezone = new \DateTimeZone(date_default_timezone_get());
118
        $expectedDate = new \DateTime($value, $expectedTimezone);
119
        $expectedDate->setTimezone($defaultTimezone);
120
121
        $resultDate = $dataTransformer->reverseTransform($value);
122
123
        $this->assertInstanceOf(\DateTime::class, $resultDate);
124
        $this->assertSame($expectedDate->format('Y-m-d'), $resultDate->format('Y-m-d'));
125
        $this->assertSame($defaultTimezone->getName(), $resultDate->getTimezone()->getName());
126
127
        // test laze-load
128
        $secondDataTransformer = $this->resolve();
129
130
        $this->assertSame($dataTransformer, $secondDataTransformer);
131
    }
132
133
    /**
134
     * @dataProvider getTimeZones
135
     */
136
    public function testResolveDateDatatimeTransformer($timezone, \DateTimeZone $expectedTimezone): void
137
    {
138
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
139
        $this->fieldDescription->getOption('timezone')->willReturn($timezone);
140
        $this->fieldDescription->getType()->willReturn('datetime');
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
141
142
        $dataTransformer = $this->resolve();
143
144
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
145
146
        $value = '2020-12-12 23:11:23';
147
        $defaultTimezone = new \DateTimeZone(date_default_timezone_get());
148
        $expectedDate = new \DateTime($value, $expectedTimezone);
149
        $expectedDate->setTimezone($defaultTimezone);
150
151
        $resultDate = $dataTransformer->reverseTransform($value);
152
153
        $this->assertInstanceOf(\DateTime::class, $resultDate);
154
        $this->assertSame($expectedDate->format('Y-m-d'), $resultDate->format('Y-m-d'));
155
        $this->assertSame($defaultTimezone->getName(), $resultDate->getTimezone()->getName());
156
157
        // test laze-load
158
        $secondDataTransformer = $this->resolve();
159
160
        $this->assertSame($dataTransformer, $secondDataTransformer);
161
    }
162
163
    public function testResolveChoiceWithoutClassName(): void
164
    {
165
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
166
        $this->fieldDescription->getType()->willReturn('choice');
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
167
        $this->fieldDescription->getOption('class')->willReturn(null);
168
169
        $this->assertNull($this->resolve());
170
    }
171
172
    public function testResolveChoiceBadClassName(): void
173
    {
174
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
175
        $this->fieldDescription->getType()->willReturn('choice');
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
176
        $this->fieldDescription->getOption('class')->willReturn(\stdClass::class);
177
        $this->fieldDescription->getTargetModel()->willReturn(\DateTime::class);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Sonata\AdminBundle\Admin\FieldDescriptionInterface as the method getTargetModel() does only exist in the following implementations of said interface: Sonata\AdminBundle\Tests...\Admin\FieldDescription, Sonata\AdminBundle\Tests...\Admin\FieldDescription.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
178
        $this->fieldDescription->getTargetEntity()->willReturn(\DateTime::class);
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getTargetEntity() (of type string|null).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
Deprecated Code introduced by
The method Sonata\AdminBundle\Admin...face::getTargetEntity() has been deprecated with message: since sonata-project/admin-bundle 3.69. Use `getTargetModel()` instead.

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...
179
180
        $this->assertNull($this->resolve());
181
    }
182
183
    public function testResolveChoice(): void
184
    {
185
        $newId = 1;
186
        $className = \stdClass::class;
187
        $object = new \stdClass();
188
189
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
190
        $this->fieldDescription->getType()->willReturn('choice');
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
191
        $this->fieldDescription->getOption('class')->willReturn($className);
192
        $this->fieldDescription->getTargetModel()->willReturn($className);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Sonata\AdminBundle\Admin\FieldDescriptionInterface as the method getTargetModel() does only exist in the following implementations of said interface: Sonata\AdminBundle\Tests...\Admin\FieldDescription, Sonata\AdminBundle\Tests...\Admin\FieldDescription.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
193
194
        $this->modelManager->find($className, $newId)->willReturn($object);
0 ignored issues
show
Bug introduced by
The method find() does not seem to exist on object<Sonata\AdminBundl...ldDescriptionInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
195
196
        $dataTransformer = $this->resolve();
197
198
        $this->assertInstanceOf(ModelToIdTransformer::class, $dataTransformer);
199
        $this->assertSame($object, $dataTransformer->reverseTransform($newId));
200
    }
201
202
    /**
203
     * @dataProvider provideFieldTypes
204
     */
205
    public function testCustomGlobalTransformers(string $fieldType): void
206
    {
207
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
208
            return (string) (int) $value;
209
        }, static function ($value): bool {
210
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
211
        });
212
213
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
214
        $this->fieldDescription->getType()->willReturn($fieldType);
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
215
216
        $this->resolver = new DataTransformerResolver([
217
            $fieldType => $customDataTransformer, // override predefined transformer
218
        ]);
219
220
        $dataTransformer = $this->resolve();
221
222
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
223
        $this->assertSame($customDataTransformer, $dataTransformer);
224
    }
225
226
    /**
227
     * @dataProvider provideFieldTypes
228
     */
229
    public function testAddCustomGlobalTransformer(string $fieldType): void
230
    {
231
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
232
            return (string) (int) $value;
233
        }, static function ($value): bool {
234
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
235
        });
236
237
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
238
        $this->fieldDescription->getType()->willReturn($fieldType);
0 ignored issues
show
Bug introduced by
The method willReturn cannot be called on $this->fieldDescription->getType() (of type integer|string).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
239
240
        $this->resolver->addCustomGlobalTransformer($fieldType, $customDataTransformer);
241
242
        $dataTransformer = $this->resolve();
243
244
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
245
        $this->assertSame($customDataTransformer, $dataTransformer);
246
    }
247
248
    protected function resolve(): ?DataTransformerInterface
249
    {
250
        return $this->resolver->resolve($this->fieldDescription->reveal(), $this->modelManager->reveal());
0 ignored issues
show
Bug introduced by
The method reveal() does not seem to exist on object<Sonata\AdminBundl...ldDescriptionInterface>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
251
    }
252
}
253