Completed
Push — 3.x ( 3e834f...38b337 )
by Grégoire
03:36
created

src/Form/FormMapper.php (1 issue)

strict.coding_against_concrete_implementation

Bug Minor

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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\Form;
15
16
use Sonata\AdminBundle\Admin\AdminInterface;
17
use Sonata\AdminBundle\Builder\FormContractorInterface;
18
use Sonata\AdminBundle\Form\Type\CollectionType;
19
use Sonata\AdminBundle\Mapper\BaseGroupedMapper;
20
use Symfony\Component\Form\Extension\Core\Type\CollectionType as SymfonyCollectionType;
21
use Symfony\Component\Form\FormBuilderInterface;
22
23
/**
24
 * This class is use to simulate the Form API.
25
 *
26
 * @final since sonata-project/admin-bundle 3.52
27
 *
28
 * @author Thomas Rabaix <[email protected]>
29
 */
30
class FormMapper extends BaseGroupedMapper
31
{
32
    /**
33
     * @var FormBuilderInterface
34
     */
35
    protected $formBuilder;
36
37
    public function __construct(
38
        FormContractorInterface $formContractor,
39
        FormBuilderInterface $formBuilder,
40
        AdminInterface $admin
41
    ) {
42
        parent::__construct($formContractor, $admin);
43
        $this->formBuilder = $formBuilder;
44
    }
45
46
    public function reorder(array $keys)
47
    {
48
        $this->admin->reorderFormGroup($this->getCurrentGroupName(), $keys);
49
50
        return $this;
51
    }
52
53
    /**
54
     * @param FormBuilderInterface|string $name
55
     * @param string|null                 $type
56
     *
57
     * @return $this
58
     */
59
    public function add($name, $type = null, array $options = [], array $fieldDescriptionOptions = [])
60
    {
61
        if (!$this->shouldApply()) {
62
            return $this;
63
        }
64
65
        if ($name instanceof FormBuilderInterface) {
66
            $fieldName = $name->getName();
67
        } else {
68
            $fieldName = $name;
69
        }
70
71
        // "Dot" notation is not allowed as form name, but can be used as property path to access nested data.
72
        if (!$name instanceof FormBuilderInterface && !isset($options['property_path'])) {
73
            $options['property_path'] = $fieldName;
74
75
            // fix the form name
76
            $fieldName = $this->sanitizeFieldName($fieldName);
77
        }
78
79
        // change `collection` to `sonata_type_native_collection` form type to
80
        // avoid BC break problems
81
        if ('collection' === $type || SymfonyCollectionType::class === $type) {
82
            $type = CollectionType::class;
83
        }
84
85
        $label = $fieldName;
86
87
        $group = $this->addFieldToCurrentGroup($label);
88
89
        // Try to autodetect type
90
        if ($name instanceof FormBuilderInterface && null === $type) {
91
            $fieldDescriptionOptions['type'] = \get_class($name->getType()->getInnerType());
92
        }
93
94
        if (!isset($fieldDescriptionOptions['type']) && \is_string($type)) {
95
            $fieldDescriptionOptions['type'] = $type;
96
        }
97
98
        if ($group['translation_domain'] && !isset($fieldDescriptionOptions['translation_domain'])) {
99
            $fieldDescriptionOptions['translation_domain'] = $group['translation_domain'];
100
        }
101
102
        $fieldDescription = $this->admin->getModelManager()->getNewFieldDescriptionInstance(
103
            $this->admin->getClass(),
104
            $name instanceof FormBuilderInterface ? $name->getName() : $name,
105
            $fieldDescriptionOptions
106
        );
107
108
        // Note that the builder var is actually the formContractor:
109
        $this->builder->fixFieldDescription($this->admin, $fieldDescription, $fieldDescriptionOptions);
110
111
        if ($fieldName !== $name) {
112
            $fieldDescription->setName($fieldName);
113
        }
114
115
        if ($name instanceof FormBuilderInterface) {
116
            $type = null;
117
            $options = [];
118
        } else {
119
            $name = $fieldDescription->getName();
120
121
            // Note that the builder var is actually the formContractor:
122
            $options = array_replace_recursive($this->builder->getDefaultOptions($type, $fieldDescription) ?? [], $options);
0 ignored issues
show
It seems like you code against a concrete implementation and not the interface Sonata\AdminBundle\Builder\BuilderInterface as the method getDefaultOptions() does only exist in the following implementations of said interface: Sonata\AdminBundle\Tests...\Builder\FormContractor.

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...
123
124
            // be compatible with mopa if not installed, avoid generating an exception for invalid option
125
            // force the default to false ...
126
            if (!isset($options['label_render'])) {
127
                $options['label_render'] = false;
128
            }
129
130
            if (!isset($options['label'])) {
131
                $options['label'] = $this->admin->getLabelTranslatorStrategy()->getLabel($name, 'form', 'label');
132
            }
133
134
            if (isset($options['help'])) {
135
                $fieldDescription->setHelp($options['help']);
136
                unset($options['help']);
137
            }
138
        }
139
140
        $this->admin->addFormFieldDescription($fieldName, $fieldDescription);
141
142
        if (!isset($fieldDescriptionOptions['role']) || $this->admin->isGranted($fieldDescriptionOptions['role'])) {
143
            $this->formBuilder->add($name, $type, $options);
144
        }
145
146
        return $this;
147
    }
148
149
    public function get($name)
150
    {
151
        $name = $this->sanitizeFieldName($name);
152
153
        return $this->formBuilder->get($name);
154
    }
155
156
    public function has($key)
157
    {
158
        $key = $this->sanitizeFieldName($key);
159
160
        return $this->formBuilder->has($key);
161
    }
162
163
    final public function keys()
164
    {
165
        return array_keys($this->formBuilder->all());
166
    }
167
168
    public function remove($key)
169
    {
170
        $key = $this->sanitizeFieldName($key);
171
        $this->admin->removeFormFieldDescription($key);
172
        $this->admin->removeFieldFromFormGroup($key);
173
        $this->formBuilder->remove($key);
174
175
        return $this;
176
    }
177
178
    /**
179
     * Removes a group.
180
     *
181
     * @param string $group          The group to delete
182
     * @param string $tab            The tab the group belongs to, defaults to 'default'
183
     * @param bool   $deleteEmptyTab Whether or not the Tab should be deleted, when the deleted group leaves the tab empty after deletion
184
     *
185
     * @return $this
186
     */
187
    public function removeGroup($group, $tab = 'default', $deleteEmptyTab = false)
188
    {
189
        $groups = $this->getGroups();
190
191
        // When the default tab is used, the tabname is not prepended to the index in the group array
192
        if ('default' !== $tab) {
193
            $group = $tab.'.'.$group;
194
        }
195
196
        if (isset($groups[$group])) {
197
            foreach ($groups[$group]['fields'] as $field) {
198
                $this->remove($field);
199
            }
200
        }
201
        unset($groups[$group]);
202
203
        $tabs = $this->getTabs();
204
        $key = array_search($group, $tabs[$tab]['groups'], true);
205
206
        if (false !== $key) {
207
            unset($tabs[$tab]['groups'][$key]);
208
        }
209
        if ($deleteEmptyTab && 0 === \count($tabs[$tab]['groups'])) {
210
            unset($tabs[$tab]);
211
        }
212
213
        $this->setTabs($tabs);
214
        $this->setGroups($groups);
215
216
        return $this;
217
    }
218
219
    /**
220
     * @return FormBuilderInterface
221
     */
222
    public function getFormBuilder()
223
    {
224
        return $this->formBuilder;
225
    }
226
227
    /**
228
     * @param string $name
229
     * @param mixed  $type
230
     *
231
     * @return FormBuilderInterface
232
     */
233
    public function create($name, $type = null, array $options = [])
234
    {
235
        return $this->formBuilder->create($name, $type, $options);
236
    }
237
238
    /**
239
     * @return FormMapper
240
     */
241
    public function setHelps(array $helps = [])
242
    {
243
        foreach ($helps as $name => $help) {
244
            $this->addHelp($name, $help);
245
        }
246
247
        return $this;
248
    }
249
250
    /**
251
     * @return FormMapper
252
     */
253
    public function addHelp($name, $help)
254
    {
255
        if ($this->admin->hasFormFieldDescription($name)) {
256
            $this->admin->getFormFieldDescription($name)->setHelp($help);
257
        }
258
259
        return $this;
260
    }
261
262
    /**
263
     * Symfony default form class sadly can't handle
264
     * form element with dots in its name (when data
265
     * get bound, the default dataMapper is a PropertyPathMapper).
266
     * So use this trick to avoid any issue.
267
     *
268
     * @param string $fieldName
269
     *
270
     * @return string
271
     */
272
    protected function sanitizeFieldName($fieldName)
273
    {
274
        return str_replace(['__', '.'], ['____', '__'], $fieldName);
275
    }
276
277
    protected function getGroups()
278
    {
279
        // NEXT_MAJOR: Remove the argument "sonata_deprecation_mute" in the following call.
280
281
        return $this->admin->getFormGroups('sonata_deprecation_mute');
282
    }
283
284
    protected function setGroups(array $groups)
285
    {
286
        $this->admin->setFormGroups($groups);
287
    }
288
289
    protected function getTabs()
290
    {
291
        // NEXT_MAJOR: Remove the argument "sonata_deprecation_mute" in the following call.
292
293
        return $this->admin->getFormTabs('sonata_deprecation_mute');
294
    }
295
296
    protected function setTabs(array $tabs)
297
    {
298
        $this->admin->setFormTabs($tabs);
299
    }
300
301
    protected function getName()
302
    {
303
        return 'form';
304
    }
305
}
306