Completed
Pull Request — 3.x (#5937)
by Peter
05:08 queued 01:40
created

testResolveDateDatatimeTransformer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 26
rs 9.504
c 0
b 0
f 0
cc 1
nc 1
nop 2
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\ModelToIdTransformer;
19
use Sonata\AdminBundle\Form\DataTransformerResolver;
20
use Sonata\AdminBundle\Model\ModelManagerInterface;
21
use Symfony\Component\Form\CallbackTransformer;
22
use Symfony\Component\Form\DataTransformerInterface;
23
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToStringTransformer;
24
25
/**
26
 * @author Peter Gribanov <[email protected]>
27
 */
28
final class DataTransformerResolverTest extends TestCase
29
{
30
    /**
31
     * @var DataTransformerResolver
32
     */
33
    private $resolver;
34
35
    /**
36
     * @var FieldDescriptionInterface
37
     */
38
    private $fieldDescription;
39
40
    /**
41
     * @var FieldDescriptionInterface
42
     */
43
    private $modelManager;
44
45
    protected function setUp(): void
46
    {
47
        $this->fieldDescription = $this->prophesize(FieldDescriptionInterface::class);
48
        $this->modelManager = $this->prophesize(ModelManagerInterface::class);
49
        $this->resolver = new DataTransformerResolver();
50
    }
51
52
    public function testFailedResolve(): void
53
    {
54
        $this->assertNull($this->resolve());
55
    }
56
57
    public function provideFieldTypes(): array
58
    {
59
        return [
60
            ['foo'],
61
            // override predefined transformers
62
            ['date'],
63
            ['boolean'],
64
            ['choice'],
65
        ];
66
    }
67
68
    /**
69
     * @dataProvider provideFieldTypes
70
     */
71
    public function testResolveCustomDataTransformer(string $fieldType): void
72
    {
73
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
74
            return (string) (int) $value;
75
        }, static function ($value): bool {
76
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
77
        });
78
        $this->fieldDescription->getOption('data_transformer')->willReturn($customDataTransformer);
79
        $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...
80
81
        $dataTransformer = $this->resolve();
82
83
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
84
        $this->assertSame($customDataTransformer, $dataTransformer);
85
    }
86
87
    public function getTimeZones(): iterable
88
    {
89
        $default = new \DateTimeZone(date_default_timezone_get());
90
        $custom = new \DateTimeZone('Europe/Rome');
91
92
        return [
93
            'empty timezone' => [null, $default],
94
            'disabled timezone' => [false, $default],
95
            'default timezone by name' => [$default->getName(), $default],
96
            'default timezone by object' => [$default, $default],
97
            'custom timezone by name' => [$custom->getName(), $custom],
98
            'custom timezone by object' => [$custom, $custom],
99
        ];
100
    }
101
102
    /**
103
     * @dataProvider getTimeZones
104
     */
105
    public function testResolveDateDataTransformer($timezone, \DateTimeZone $expectedTimezone): void
106
    {
107
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
108
        $this->fieldDescription->getOption('timezone')->willReturn($timezone);
109
        $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...
110
111
        $dataTransformer = $this->resolve();
112
113
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
114
115
        $value = '2020-12-12';
116
        $defaultTimezone = new \DateTimeZone(date_default_timezone_get());
117
        $expectedDate = new \DateTime($value, $expectedTimezone);
118
        $expectedDate->setTimezone($defaultTimezone);
119
120
        $resultDate = $dataTransformer->reverseTransform($value);
121
122
        $this->assertInstanceOf(\DateTime::class, $resultDate);
123
        $this->assertSame($expectedDate->format('Y-m-d'), $resultDate->format('Y-m-d'));
124
        $this->assertSame($defaultTimezone->getName(), $resultDate->getTimezone()->getName());
125
126
        // test laze-load
127
        $secondDataTransformer = $this->resolve();
128
129
        $this->assertSame($dataTransformer, $secondDataTransformer);
130
    }
131
132
    /**
133
     * @dataProvider getTimeZones
134
     */
135
    public function testResolveDateDatatimeTransformer($timezone, \DateTimeZone $expectedTimezone): void
136
    {
137
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
138
        $this->fieldDescription->getOption('timezone')->willReturn($timezone);
139
        $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...
140
141
        $dataTransformer = $this->resolve();
142
143
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
144
145
        $value = '2020-12-12 23:11:23';
146
        $defaultTimezone = new \DateTimeZone(date_default_timezone_get());
147
        $expectedDate = new \DateTime($value, $expectedTimezone);
148
        $expectedDate->setTimezone($defaultTimezone);
149
150
        $resultDate = $dataTransformer->reverseTransform($value);
151
152
        $this->assertInstanceOf(\DateTime::class, $resultDate);
153
        $this->assertSame($expectedDate->format('Y-m-d'), $resultDate->format('Y-m-d'));
154
        $this->assertSame($defaultTimezone->getName(), $resultDate->getTimezone()->getName());
155
156
        // test laze-load
157
        $secondDataTransformer = $this->resolve();
158
159
        $this->assertSame($dataTransformer, $secondDataTransformer);
160
    }
161
162
    public function testResolveChoiceWithoutClassName(): void
163
    {
164
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
165
        $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...
166
        $this->fieldDescription->getOption('class')->willReturn(null);
167
168
        $this->assertNull($this->resolve());
169
    }
170
171
    public function testResolveChoiceBadClassName(): void
172
    {
173
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
174
        $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...
175
        $this->fieldDescription->getOption('class')->willReturn(\stdClass::class);
176
        $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...
177
        $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...
178
179
        $this->assertNull($this->resolve());
180
    }
181
182
    public function testResolveChoice(): void
183
    {
184
        $newId = 1;
185
        $className = \stdClass::class;
186
        $object = new \stdClass();
187
188
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
189
        $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...
190
        $this->fieldDescription->getOption('class')->willReturn($className);
191
        $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...
192
193
        $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...
194
195
        $dataTransformer = $this->resolve();
196
197
        $this->assertInstanceOf(ModelToIdTransformer::class, $dataTransformer);
198
        $this->assertSame($object, $dataTransformer->reverseTransform($newId));
199
    }
200
201
    /**
202
     * @dataProvider provideFieldTypes
203
     */
204
    public function testCustomGlobalTransformers(string $fieldType): void
205
    {
206
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
207
            return (string) (int) $value;
208
        }, static function ($value): bool {
209
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
210
        });
211
212
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
213
        $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...
214
215
        $this->resolver = new DataTransformerResolver([
216
            $fieldType => $customDataTransformer, // override predefined transformer
217
        ]);
218
219
        $dataTransformer = $this->resolve();
220
221
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
222
        $this->assertSame($customDataTransformer, $dataTransformer);
223
    }
224
225
    /**
226
     * @dataProvider provideFieldTypes
227
     */
228
    public function testAddCustomGlobalTransformer(string $fieldType): void
229
    {
230
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
231
            return (string) (int) $value;
232
        }, static function ($value): bool {
233
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
234
        });
235
236
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
237
        $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...
238
239
        $this->resolver->addCustomGlobalTransformer($fieldType, $customDataTransformer);
240
241
        $dataTransformer = $this->resolve();
242
243
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
244
        $this->assertSame($customDataTransformer, $dataTransformer);
245
    }
246
247
    protected function resolve(): ?DataTransformerInterface
248
    {
249
        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...
250
    }
251
}
252