FormMapper::assertFunctionParamCanBeNull()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 3
crap 1
1
<?php
2
3
namespace MikeSimonson\EntityFormMapper;
4
5
6
use MikeSimonson\EntityFormMapper\Exception\FormMapperException;
7
use Symfony\Component\Form\Button;
8
use Symfony\Component\Form\DataMapperInterface;
9
use Symfony\Component\Form\FormError;
10
use Zend\Code\Generator\ClassGenerator;
11
use Zend\Code\Reflection\ClassReflection;
12
use MikeSimonson\EntityFormMapper\Exception\InvalidArgumentFormMapperException;
13
14
class FormMapper implements DataMapperInterface
15
{
16
17 21
    public function mapDataToForms($data, $form)
18
    {
19 21
        $form = iterator_to_array($form);
20 21
        foreach($form as $key => $formElement) {
21 21
            if ($formElement instanceof Button) {
22 3
                continue;
23
            }
24 21
            if ($data) {
25 21
                $form[$key]->setData($this->getEntityData($data, $key, $form[$key]->getParent()->getConfig()->getName()));
26
            }
27
        }
28
29 21
        return $form;
30
    }
31
32 21
    public function mapFormsToData($form, &$data)
33
    {
34 21
        $form = iterator_to_array($form);
35 21
        $formElements = array_keys($form);
36
37
        //Get The class that is mapped to this form
38 21
        $class = $form[$formElements[0]]->getParent()->getConfig()->getDataClass();
39
40
        //Determine if we are updating an entity of creating a new one
41 21
        $isUpdatingEntity = $data instanceof $class;
42
43
        try {
44 21
            if (!$isUpdatingEntity) { //trying to create a new object
45 19
                $data = $this->instantiateObject($class, $form);
46
            }
47 20
            $this->setAllThePropertiesOnTheObject($data, $form);
48 4
        } catch (InvalidArgumentFormMapperException $e) {
49 2
            $formElement = reset($form);
50 2
            $formElement->getParent()->addError(new FormError($e->getMessage()));
51 2
            if (!$isUpdatingEntity) { //Trying to create a new object
52 1
                $data = null;
53
            }
54
        }
55 20
    }
56
57 20
    private function setAllThePropertiesOnTheObject($obj, $form)
58
    {
59 20
        foreach($form as $propertyName => $formElement) {
60 20
            if ($formElement instanceof Button) {
61 3
                continue;
62
            }
63
            /**
64
             * TODO add test where it should fail with a parent entity when the class is defined outside the foreach
65
             */
66 20
            $setterName = 'set' . ucfirst($propertyName);
67 20
            $class = get_class($obj);
68 20
            $class = $this->getClassImplementingMethod($class, $setterName);
69
70 19
            $isParamTypeHint = $this->getTypeHintFromMethodParam($class, $setterName);
71
72
            /**
73
             * If form value == null
74
             * and value not required
75
             * and parameter typehinted
76
             * we skip the set.
77
             */
78 19
            $data = $formElement->getData();
79 19
            $isFormElementRequired = $formElement->getConfig()->getRequired();
80 19
            $isParamAllowingNullValue = $this->getAcceptNullForSetter($class, $setterName);
81 19
            if ($isFormElementRequired === false && $data === null && $isParamTypeHint !== null) {
82 1
                continue;
83
            }
84
85 19
            if ($isFormElementRequired === true && $data === null && $isParamTypeHint !== null && $isParamAllowingNullValue === false) {
86 1
                throw new InvalidArgumentFormMapperException($propertyName . ' is required.');
87
            }
88
89 19
            $obj->{$setterName}($data);
90
        }
91 18
    }
92
93 21
    private function getClassImplementingMethod($class, $method)
94
    {
95 21
        $reflectionClass = new ClassReflection($class);
96 21
        $reflectionClassGenerator = ClassGenerator::fromReflection($reflectionClass);
97 21
        if ($reflectionClassGenerator->getMethod($method) === false) {
98 6
            if ($reflectionClass->getParentClass() === false) {
99 2
                throw new FormMapperException('Unable to find the method ' . $method);
100
            }
101
102 4
            return $this->getClassImplementingMethod($reflectionClass->getParentClass()->getName(), $method);
0 ignored issues
show
Bug introduced by
Consider using $reflectionClass->getParentClass()->name. There is an issue with getName() and APC-enabled PHP versions.
Loading history...
103
        }
104
105 20
        return $class;
106
    }
107
108 19
    private function instantiateObject($class, $form)
109
    {
110 19
        $reflectionClass = new \ReflectionClass($class);
111 19
        $reflectionConstructor = $reflectionClass->getConstructor();
112 19
        if ($reflectionConstructor === null) {
113 11
            return $reflectionClass->newInstanceWithoutConstructor();
114
        }
115 10
        $reflectionParameters = $reflectionConstructor->getParameters();
116 10
        $nbOfRequiredParameters = $reflectionConstructor->getNumberOfRequiredParameters();
117 10
        $params = [];
118 10
        $i=0;
119 10
        foreach($reflectionParameters as $name => $param) {
120 9
            if ($i === $nbOfRequiredParameters) {
121 1
                break;
122
            }
123 9
            $i++;
124
            
125 9
            $classWithConstructor = $this->getClassImplementingMethod($class, '__construct');
126
127 9
            if ($form[$param->getName()]->getData() === null
128 9
                && !$this->assertFunctionParamCanBeNull($classWithConstructor, '__construct', $param)) {
129 1
                throw new InvalidArgumentFormMapperException('The parameter ' . $param->getName() . ' from the constructor of the  class ' .
130 1
                $class . ' cannot be null');
131
            }
132 8
            $params[] = $form[$param->getName()]->getData();
133
        }
134
135 9
        return $reflectionClass->newInstanceArgs($params);
136
    }
137
138
    /**
139
     * We are not taking into account the fact that a typehint parameter can accept null as it shouldn't be in 
140
     * the constructor if it can be set to null.
141
     * 
142
     * @param $classWithConstructor
143
     * @param $function
144
     * @param $parameter
145
     * @return bool
146
     */
147 3
    private function assertFunctionParamCanBeNull($classWithConstructor, $function, $parameter)
148
    {
149 3
        return $this->getTypeHintFromMethodParam($classWithConstructor, $function, $parameter) === null;
150
    }
151
152 15
    private function getEntityData($data, $propertyName, $formName) {
153 15
        $getterName = 'get' . ucfirst($propertyName);
154 15
        if (is_callable([$data, $getterName])) {
155 14
            return $data->{$getterName}();
156
        }
157
158 1
        throw new FormMapperException('Unable to find a getter for the property ' . $propertyName . ' on the form ' . $formName . '.');
159
    }
160
161 20
    private function getTypeHintFromMethodParam($class, $methodName, $param = null)
162
    {
163 20
        $reflectionClass = new ClassReflection($class);
164 20
        $reflectionClass = ClassGenerator::fromReflection($reflectionClass);
165 20
        $method = $reflectionClass->getMethod($methodName);
166 20
        $parameters = $method->getParameters();
167
168 20
        if ($param === null) {
169 19
            return array_values($parameters)[0]->getType();
170
        }
171
172 3
        return $parameters[$param->getName()]->getType();
173
    }
174
175 19
    private function getAcceptNullForSetter($class, $setterName)
176
    {
177 19
        $reflectionClass = new ClassReflection($class);
178 19
        $reflectionClass = ClassGenerator::fromReflection($reflectionClass);
179 19
        $method = $reflectionClass->getMethod($setterName)->getSourceContent();
180 19
        $posOpenParenthesis = stripos($method, '(') + 1;
181 19
        $posClosingParenthesis = stripos($method, ')');
182 19
        $parameter = substr($method, $posOpenParenthesis, $posClosingParenthesis - $posOpenParenthesis);
183 19
        $parameterPart = explode('=', $parameter);
184 19
        if (!isset($parameterPart[1])) {
185 19
            return false;
186
        }
187
188 5
        return strtolower(trim($parameterPart[1])) === 'null';
189
    }
190
}
191