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

DataTransformerResolverTest   A

Complexity

Total Complexity 12

Size/Duplication

Total Lines 194
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Importance

Changes 0
Metric Value
wmc 12
lcom 1
cbo 3
dl 0
loc 194
rs 10
c 0
b 0
f 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A setUp() 0 6 1
A testFailedResolve() 0 4 1
A provideFieldTypes() 0 10 1
A testResolveCustomDataTransformer() 0 15 1
A getTimeZones() 0 14 1
A testResolveDateDataTransformer() 0 26 1
A testResolveChoiceWithoutClassName() 0 8 1
A testResolveChoiceBadClassName() 0 10 1
A testResolveChoice() 0 18 1
A testCustomGlobalTransformers() 0 20 1
A testAddCustomGlobalTransformer() 0 18 1
A resolve() 0 4 1
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
    public function testResolveChoiceWithoutClassName(): void
133
    {
134
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
135
        $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...
136
        $this->fieldDescription->getOption('class')->willReturn(null);
137
138
        $this->assertNull($this->resolve());
139
    }
140
141
    public function testResolveChoiceBadClassName(): void
142
    {
143
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
144
        $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...
145
        $this->fieldDescription->getOption('class')->willReturn(\stdClass::class);
146
        $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...
147
        $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...
148
149
        $this->assertNull($this->resolve());
150
    }
151
152
    public function testResolveChoice(): void
153
    {
154
        $newId = 1;
155
        $className = \stdClass::class;
156
        $object = new \stdClass();
157
158
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
159
        $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...
160
        $this->fieldDescription->getOption('class')->willReturn($className);
161
        $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...
162
163
        $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...
164
165
        $dataTransformer = $this->resolve();
166
167
        $this->assertInstanceOf(ModelToIdTransformer::class, $dataTransformer);
168
        $this->assertSame($object, $dataTransformer->reverseTransform($newId));
169
    }
170
171
    /**
172
     * @dataProvider provideFieldTypes
173
     */
174
    public function testCustomGlobalTransformers(string $fieldType): void
175
    {
176
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
177
            return (string) (int) $value;
178
        }, static function ($value): bool {
179
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
180
        });
181
182
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
183
        $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...
184
185
        $this->resolver = new DataTransformerResolver([
186
            $fieldType => $customDataTransformer, // override predefined transformer
187
        ]);
188
189
        $dataTransformer = $this->resolve();
190
191
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
192
        $this->assertSame($customDataTransformer, $dataTransformer);
193
    }
194
195
    /**
196
     * @dataProvider provideFieldTypes
197
     */
198
    public function testAddCustomGlobalTransformer(string $fieldType): void
199
    {
200
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
201
            return (string) (int) $value;
202
        }, static function ($value): bool {
203
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
204
        });
205
206
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
207
        $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...
208
209
        $this->resolver->addCustomGlobalTransformer($fieldType, $customDataTransformer);
210
211
        $dataTransformer = $this->resolve();
212
213
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
214
        $this->assertSame($customDataTransformer, $dataTransformer);
215
    }
216
217
    protected function resolve(): ?DataTransformerInterface
218
    {
219
        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...
220
    }
221
}
222