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

src/Form/FormMapper.php (2 issues)

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);
0 ignored issues
show
The call to BuilderInterface::fixFieldDescription() has too many arguments starting with $fieldDescriptionOptions.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
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