Completed
Pull Request — 3.x (#5937)
by Peter
03:13
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 testResolveDateDataTransformer(): void
89
    {
90
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
91
        $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...
92
93
        $dataTransformer = $this->resolve();
94
95
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
96
97
        // test laze-load
98
        $secondDataTransformer = $this->resolve();
99
100
        $this->assertSame($dataTransformer, $secondDataTransformer);
101
    }
102
103
    public function testResolveDateDatatimeTransformer(): void
104
    {
105
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
106
        $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...
107
108
        $dataTransformer = $this->resolve();
109
110
        $this->assertInstanceOf(DateTimeToStringTransformer::class, $dataTransformer);
111
112
        // test laze-load
113
        $secondDataTransformer = $this->resolve();
114
115
        $this->assertSame($dataTransformer, $secondDataTransformer);
116
    }
117
118
    public function testResolveBooleanDataTransformer(): void
119
    {
120
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
121
        $this->fieldDescription->getType()->willReturn('boolean');
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...
122
123
        $dataTransformer = $this->resolve();
124
125
        $this->assertInstanceOf(BooleanToStringTransformer::class, $dataTransformer);
126
        $this->assertSame('1', $dataTransformer->transform(true));
127
128
        // test laze-load
129
        $secondDataTransformer = $this->resolve();
130
131
        $this->assertSame($dataTransformer, $secondDataTransformer);
132
    }
133
134
    public function testResolveChoiceWithoutClassName(): void
135
    {
136
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
137
        $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...
138
        $this->fieldDescription->getOption('class')->willReturn(null);
139
140
        $this->assertNull($this->resolve());
141
    }
142
143
    public function testResolveChoiceBadClassName(): void
144
    {
145
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
146
        $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...
147
        $this->fieldDescription->getOption('class')->willReturn(\stdClass::class);
148
        $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...
149
150
        $this->assertNull($this->resolve());
151
    }
152
153
    public function testResolveChoice(): void
154
    {
155
        $newId = 1;
156
        $className = \stdClass::class;
157
        $object = new \stdClass();
158
159
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
160
        $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...
161
        $this->fieldDescription->getOption('class')->willReturn($className);
162
        $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...
163
164
        $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...
165
166
        $dataTransformer = $this->resolve();
167
168
        $this->assertInstanceOf(ModelToIdTransformer::class, $dataTransformer);
169
        $this->assertSame($object, $dataTransformer->reverseTransform($newId));
170
    }
171
172
    /**
173
     * @dataProvider provideFieldTypes
174
     */
175
    public function testCustomGlobalTransformers(string $fieldType): void
176
    {
177
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
178
            return (string) (int) $value;
179
        }, static function ($value): bool {
180
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
181
        });
182
183
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
184
        $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...
185
186
        $this->resolver = new DataTransformerResolver([
187
            $fieldType => $customDataTransformer, // override predefined transformer
188
        ]);
189
190
        $dataTransformer = $this->resolve();
191
192
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
193
        $this->assertSame($customDataTransformer, $dataTransformer);
194
    }
195
196
    /**
197
     * @dataProvider provideFieldTypes
198
     */
199
    public function testAddCustomGlobalTransformer(string $fieldType): void
200
    {
201
        $customDataTransformer = new CallbackTransformer(static function ($value): string {
202
            return (string) (int) $value;
203
        }, static function ($value): bool {
204
            return filter_var($value, FILTER_VALIDATE_BOOLEAN);
205
        });
206
207
        $this->fieldDescription->getOption('data_transformer')->willReturn(null);
208
        $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...
209
210
        $this->resolver->addCustomGlobalTransformer($fieldType, $customDataTransformer);
211
212
        $dataTransformer = $this->resolve();
213
214
        $this->assertInstanceOf(DataTransformerInterface::class, $dataTransformer);
215
        $this->assertSame($customDataTransformer, $dataTransformer);
216
    }
217
218
    protected function resolve(): ?DataTransformerInterface
219
    {
220
        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...
221
    }
222
}
223