FormDataConstraintFinder   C
last analyzed

Complexity

Total Complexity 55

Size/Duplication

Total Lines 306
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 10
dl 0
loc 306
ccs 0
cts 144
cp 0
rs 6.8
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 11 3
D find() 0 97 17
B findPropertyConstraints() 0 28 5
C resolveDataClass() 0 35 8
A resolveDataSource() 0 18 4
A addCascadingValidConstraint() 0 12 4
A camelize() 0 4 1
A guessProperty() 0 15 3
D findPropertyDataTypeInfo() 0 35 10

How to fix   Complexity   

Complex Class

Complex classes like FormDataConstraintFinder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use FormDataConstraintFinder, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace Boekkooi\Bundle\JqueryValidationBundle\Form;
3
4
use Boekkooi\Bundle\JqueryValidationBundle\Exception\UnsupportedException;
5
use Boekkooi\Bundle\JqueryValidationBundle\Validator\ConstraintCollection;
6
use Symfony\Component\Form\FormInterface;
7
use Symfony\Component\Validator\Constraints\Type;
8
use Symfony\Component\Validator\Constraints\Valid;
9
use Symfony\Component\Validator\Mapping\CascadingStrategy;
10
use Symfony\Component\Validator\Mapping\ClassMetadata;
11
use Symfony\Component\Validator\Mapping\MemberMetadata;
12
use Symfony\Component\Validator\Mapping\Factory\MetadataFactoryInterface;
13
14
/**
15
 * @author Warnar Boekkooi <[email protected]>
16
 */
17
class FormDataConstraintFinder
18
{
19
    /**
20
     * @var MetadataFactoryInterface
21
     */
22
    private $metadataFactory;
23
24
    /**
25
     * Constructor.
26
     * @param MetadataFactoryInterface $metadataFactory
27
     */
28
    public function __construct($metadataFactory)
29
    {
30
        if (
31
            !$metadataFactory instanceof MetadataFactoryInterface &&
32
            !$metadataFactory instanceof \Symfony\Component\Validator\MetadataFactoryInterface
33
        ) {
34
            throw new \InvalidArgumentException('metadataFactory must be a instanceof MetadataFactoryInterface');
35
        }
36
37
        $this->metadataFactory = $metadataFactory;
38
    }
39
40
    public function find(FormInterface $form)
41
    {
42
        $propertyPath = $form->getPropertyPath();
43
        if ($form->getPropertyPath() === null) {
44
            return new ConstraintCollection();
45
        }
46
47
        $class = $this->resolveDataClass($form);
48
        if ($class === null) {
49
            return new ConstraintCollection();
50
        }
51
52
        $metadata = $this->metadataFactory->getMetadataFor($class);
53
        if (!$metadata instanceof ClassMetadata) {
54
            return new ConstraintCollection();
55
        }
56
57
        if ($propertyPath->getLength() < 1) {
58
            throw new UnsupportedException('Not supported please submit a issue with the form that produces this error!');
59
        }
60
61
        // Retrieve the last property element
62
        $propertyLastElementIndex = $propertyPath->getLength() - 1;
63
        $propertyName = $propertyPath->getElement($propertyLastElementIndex);
64
65
        if ($propertyPath->getLength() > 1) {
66
            // When we have multiple parts to the path then resolve it
67
            // To return the actual property and metadata
68
69
            // Resolve parent data
70
            list($dataSource, $dataSourceClass) = $this->resolveDataSource($form);
71
            for ($i = 0; $i < $propertyPath->getLength() - 1; $i++) {
72
                $element = $propertyPath->getElement($i);
73
74
                $property = $this->guessProperty($metadata, $element);
75
76
                // If the Valid tag is missing the property will return null.
77
                // Or if there is no data set on the form
78
                if ($property === null) {
79
                    return new ConstraintCollection();
80
                }
81
82
                foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
83
                    if (!$propertyMetadata instanceof MemberMetadata) {
84
                        continue;
85
                    }
86
87
                    $dataSourceInfo = $this->findPropertyDataTypeInfo($propertyMetadata, $dataSource, $dataSourceClass);
88
                    if ($dataSourceInfo === null) {
89
                        return new ConstraintCollection();
90
                    }
91
                    list($dataSourceClass, $dataSource) = $dataSourceInfo;
92
93
                    // Handle arrays/index based properties
94
                    while ($dataSourceClass === null) {
95
                        $i++;
96
                        if (!$propertyPath->isIndex($i)) {
97
                            // For some strange reason the findPropertyDataTypeInfo is wrong
98
                            // or the form is wrong
99
                            return new ConstraintCollection();
100
                        }
101
102
                        $dataSource = $dataSource[$propertyPath->getElement($i)];
103
                        if (is_object($dataSource)) {
104
                            $dataSourceClass = get_class($dataSource);
105
                        }
106
                    }
107
108
                    // Ok we failed to find the data source class
109
                    if ($dataSourceClass === null) {
110
                        return new ConstraintCollection();
111
                    }
112
113
                    $metadata = $this->metadataFactory->getMetadataFor($dataSourceClass);
114
                    if (!$metadata instanceof ClassMetadata) {
115
                        continue;
116
                    }
117
                    continue 2;
118
                }
119
120
                // We where unable to locate a class/array property
121
                return new ConstraintCollection();
122
            }
123
        }
124
125
        // Handle array properties
126
        $propertyCascadeOnly = false;
127
        if ($propertyPath->isIndex($propertyLastElementIndex)) {
128
            $propertyCascadeOnly = true;
129
130
            $elements = $form->getParent()->getPropertyPath()->getElements();
131
            $propertyName = end($elements);
132
        }
133
134
        // Find property constraints
135
        return $this->findPropertyConstraints($metadata, $propertyName, $propertyCascadeOnly);
136
    }
137
138
    private function findPropertyConstraints(ClassMetadata $metadata, $propertyName, $cascadingOnly = false)
139
    {
140
        $constraintCollection = new ConstraintCollection();
141
142
        $property = $this->guessProperty($metadata, $propertyName);
143
        if ($property === null) {
144
            return $constraintCollection;
145
        }
146
147
        foreach ($metadata->getPropertyMetadata($property) as $propertyMetadata) {
148
            if (!$propertyMetadata instanceof MemberMetadata) {
149
                continue;
150
            }
151
152
            // For some reason Valid constraint is not in the list of constraints so we hack it in ....
153
            $this->addCascadingValidConstraint($propertyMetadata, $constraintCollection);
154
            if ($cascadingOnly) {
155
                continue;
156
            }
157
158
            // Add the actual constraints
159
            $constraintCollection->addCollection(
160
                new ConstraintCollection($propertyMetadata->getConstraints())
161
            );
162
        }
163
164
        return $constraintCollection;
165
    }
166
167
    /**
168
     * Gets the form root data class used by the given form.
169
     *
170
     * @param FormInterface $form
171
     * @return string|null
172
     */
173
    private function resolveDataClass(FormInterface $form)
174
    {
175
        // Nothing to do if root
176
        if ($form->isRoot()) {
177
            return $form->getConfig()->getDataClass();
178
        }
179
180
        $propertyPath = $form->getPropertyPath();
181
        /** @var FormInterface $dataForm */
182
        $dataForm = $form;
183
184
        // If we have a index then we need to use it's parent
185
        if ($propertyPath->getLength() === 1 && $propertyPath->isIndex(0) && $form->getConfig()->getCompound()) {
186
            return $this->resolveDataClass($form->getParent());
187
        }
188
189
        // Now locate the closest data class
190
        // TODO what is the length really for?
191
        for ($i = $propertyPath->getLength(); $i !== 0; $i--) {
192
            $dataForm = $dataForm->getParent();
193
194
            # When a data class is found then use that form
195
            # This happend when property_path contains multiple parts aka `entity.prop`
196
            if ($dataForm->getConfig()->getDataClass() !== null) {
197
                break;
198
            }
199
        }
200
201
        // If the root inherits data, then grab the parent
202
        if ($dataForm->getConfig()->getInheritData()) {
203
            $dataForm = $dataForm->getParent();
204
        }
205
206
        return $dataForm->getConfig()->getDataClass();
207
    }
208
209
    /**
210
     * Gets the form data to which a property path applies
211
     *
212
     * @param FormInterface $form
213
     * @return object|null
214
     */
215
    private function resolveDataSource(FormInterface $form)
216
    {
217
        if ($form->isRoot()) {
218
            // Nothing to do if root
219
            $dataForm = $form->getData();
220
        } else {
221
            $dataForm = $form;
222
            while ($dataForm->getConfig()->getDataClass() === null) {
223
                $dataForm = $form->getParent();
224
            }
225
        }
226
227
        $data = $dataForm->getData();
228
        return array(
229
            $data,
230
            $data === null ? $dataForm->getConfig()->getDataClass() : get_class($data)
231
        );
232
    }
233
234
    private function addCascadingValidConstraint(MemberMetadata $propertyMetadata, ConstraintCollection $constraintCollection)
235
    {
236
        if (method_exists($propertyMetadata, 'getCascadingStrategy')) {
237
            if ($propertyMetadata->getCascadingStrategy() === CascadingStrategy::CASCADE) {
238
                $constraintCollection->add(new Valid());
239
            }
240
        } else {
241
            if ($propertyMetadata->isCollectionCascaded()) {
242
                $constraintCollection->add(new Valid());
243
            }
244
        }
245
    }
246
247
    /**
248
     * Returns the lowerCamelCase form of a string.
249
     *
250
     * @param string $string The string to camelize.
251
     * @return string The camelized version of the string
252
     */
253
    private function camelize($string)
254
    {
255
        return lcfirst(strtr(ucwords(strtr($string, array('_' => ' '))), array(' ' => '')));
256
    }
257
258
    /**
259
     * Guess what property a given element belongs to.
260
     *
261
     * @param ClassMetadata $metadata
262
     * @param string $element
263
     * @return null|string
264
     */
265
    private function guessProperty(ClassMetadata $metadata, $element)
266
    {
267
        // Is it the element the actual property
268
        if ($metadata->hasPropertyMetadata($element)) {
269
            return $element;
270
        }
271
272
        // Is it a camelized property
273
        $camelized = $this->camelize($element);
274
        if ($metadata->hasPropertyMetadata($camelized)) {
275
            return $camelized;
276
        }
277
278
        return null;
279
    }
280
281
    /**
282
     * @param MemberMetadata $propertyMetadata
283
     * @param mixed $dataSource
284
     * @param string $dataSourceClass
285
     * @return null|array
286
     */
287
    protected function findPropertyDataTypeInfo(MemberMetadata $propertyMetadata, $dataSource, $dataSourceClass)
288
    {
289
        if ($dataSource !== null) {
290
            $dataSource = $propertyMetadata
291
                ->getReflectionMember($dataSourceClass)
292
                ->getValue($dataSource);
293
294
            if (is_array($dataSource) || $dataSource instanceof \ArrayAccess) {
295
                return array(null, $dataSource);
296
            }
297
            if (is_object($dataSource)) {
298
                return array(get_class($dataSource), $dataSource);
299
            }
300
            return null;
301
        }
302
303
        // Since there is no datasource we need another way to determin the properties class
304
        foreach ($propertyMetadata->getConstraints() as $constraint) {
305
            if (!$constraint instanceof Type) {
306
                continue;
307
            }
308
309
            $type = strtolower($constraint->type);
310
            $type = $type === 'boolean' ? 'bool' : $constraint->type;
311
            $isFunction = 'is_' . $type;
312
            $ctypeFunction = 'ctype_' . $type;
313
            if (function_exists($isFunction) || function_exists($ctypeFunction)) {
314
                return null;
315
            }
316
317
            return array($constraint->type, null);
318
        }
319
320
        return null;
321
    }
322
}
323