Completed
Push — master ( e44a09...23fc75 )
by Rudie
22s queued 11s
created

CollectionType::formatInputIntoModels()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 4

Importance

Changes 0
Metric Value
cc 4
nc 4
nop 2
dl 0
loc 18
ccs 10
cts 10
cp 1
crap 4
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
namespace Kris\LaravelFormBuilder\Fields;
4
5
use Illuminate\Support\Collection;
6
7
class CollectionType extends ParentType
8
{
9
    /**
10
     * Contains template for a collection element.
11
     *
12
     * @var FormField
13
     */
14
    protected $proto;
15
16
    /**
17
     * @inheritdoc
18
     */
19
    protected $valueProperty = 'data';
20
21
    /**
22
     * @return string
23
     */
24 13
    protected function getTemplate()
25
    {
26 13
        return 'collection';
27
    }
28
29
    /**
30
     * @inheritdoc
31
     */
32 13
    protected function getDefaults()
33
    {
34
        return [
35 13
            'type' => null,
36
            'options' => ['is_child' => true],
37
            'prototype' => true,
38
            'data' => null,
39
            'property' => 'id',
40
            'prototype_name' => '__NAME__',
41
            'empty_row' => true,
42
            'prefer_input' => false,
43
            'empty_model' => null,
44
        ];
45
    }
46
47
    /**
48
     * Get the prototype object.
49
     *
50
     * @return FormField
51
     * @throws \Exception
52 4
     */
53
    public function prototype()
54
    {
55 4
56 1
        if ($this->getOption('prototype') === false) {
57 1
            throw new \Exception(
58
                'Prototype for collection field [' . $this->name .'] is disabled.'
59
            );
60
        }
61 3
62
        return $this->proto;
63
    }
64
65
    /**
66
     * @inheritdoc
67
     */
68
    public function getAllAttributes()
69
    {
70
        // Collect all children's attributes.
71
        return $this->parent->getFormHelper()->mergeAttributes($this->children);
72
    }
73
74
    /**
75
     * @inheritdoc
76 13
     */
77
    protected function createChildren()
78 13
    {
79 13
        $this->children = [];
80 13
        $type = $this->getOption('type');
81 13
        $oldInput = $this->parent->getRequest()->old($this->getNameKey());
82
        $currentInput = $this->parent->getRequest()->input($this->getNameKey());
83 13
84 13
        is_array($oldInput) or $oldInput = [];
85
        is_array($currentInput) or $currentInput = [];
86
87 13
        try {
88 1
            $fieldType = $this->formHelper->getFieldType($type);
89 1
        } catch (\Exception $e) {
90 1
            throw new \Exception(
91 1
                'Collection field ['.$this->name.'] requires [type] option'. "\n\n".
92
                $e->getMessage()
93
            );
94
        }
95 12
96
        $data = $this->getOption($this->valueProperty, []);
97
98 12
        // If no value is provided, get values from current request.
99
        if (!is_null($data) && count($data) === 0) {
100
            $data = $currentInput;
101
        }
102 12
        // Or if the current request input is preferred over original data.
103
        elseif ($this->getOption('prefer_input') && count($currentInput)) {
104
            $data = $currentInput;
105
        }
106
107
        if ($data instanceof Collection) {
108 12
            $data = $data->all();
109 1
        }
110
111
        // Needs to have more than 1 item because 1 is rendered by default.
112 12
        // This overrides current request in situations when validation fails.
113 3
        if ($oldInput && count($oldInput) > 1) {
114
            $data = $this->formatInputIntoModels($oldInput, $data);
0 ignored issues
show
Bug introduced by
It seems like $oldInput defined by $this->parent->getReques...ld($this->getNameKey()) on line 81 can also be of type string; however, Kris\LaravelFormBuilder\...formatInputIntoModels() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
115
        }
116 12
117
        $field = new $fieldType($this->name, $type, $this->parent, $this->getOption('options'));
118 12
119 11
        if ($this->getOption('prototype')) {
120
            $this->generatePrototype(clone $field);
121
        }
122 12
123 4
        if (!$data || empty($data)) {
124 3
            if ($this->getOption('empty_row')) {
125 3
                return $this->children[] = $this->setupChild(clone $field, '[0]', $this->makeEmptyRowValue());
126
            }
127
128 1
            return $this->children = [];
129
        }
130
131 8
        if (!is_array($data) && !$data instanceof \Traversable) {
132 1
            throw new \Exception(
133 1
                'Data for collection field ['.$this->name.'] must be iterable.'
134
            );
135
        }
136
137 7
        foreach ($data as $key => $val) {
138 7
            $this->children[] = $this->setupChild(clone $field, '['.$key.']', $val);
139
        }
140 7
    }
141
142
    protected function makeEmptyRowValue()
143
    {
144
        $empty = $this->getOption('empty_row');
145
        return $empty === true ? $this->makeNewEmptyModel() : $empty;
146
    }
147
148
    protected function makeNewEmptyModel()
149
    {
150 12
        return value($this->getOption('empty_model'));
151
    }
152 12
153
    protected function formatInputIntoModels(array $input, array $originalData = [])
154 12
    {
155 12
        if (!$this->getOption('empty_model')) {
156 12
            return $input;
157
        }
158
159 12
        $newData = [];
160 12
        foreach ($input as $k => $inputItem) {
161
            if (is_array($inputItem)) {
162 12
                $newData[$k] = tap($originalData[$k] ?? $this->makeNewEmptyModel())->forceFill($inputItem);
163 2
            }
164 2
            else {
165 2
                $newData[$k] = $inputItem;
166
            }
167
        }
168
169 12
        return $newData;
170
    }
171
172 12
    /**
173
     * Set up a single child element for a collection.
174
     *
175
     * @param FormField $field
176
     * @param           $name
177
     * @param null      $value
178
     * @return FormField
179
     */
180
    protected function setupChild(FormField $field, $name, $value = null)
181 11
    {
182
        $newFieldName = $field->getName().$name;
183 11
184 11
        $firstFieldOptions = $this->formHelper->mergeOptions(
185 11
            $this->getOption('options'),
186
            ['attr' => ['id' => $newFieldName]]
187 11
        );
188 6
189 5
        $field->setName($newFieldName);
190 5
        $field->setOptions($firstFieldOptions);
191
192
        if ($value && !$field instanceof ChildFormType) {
193
            $value = $this->getModelValueAttribute(
194
                $value,
195 11
                $this->getOption('property')
196 11
            );
197
        }
198
199
        $field->setValue($value);
200
201
202
        return $field;
203 11
    }
204
205 11
    /**
206
     * Generate prototype for regular form field.
207
     *
208
     * @param FormField $field
209
     * @return void
210
     */
211
    protected function generatePrototype(FormField $field)
212
    {
213
        $value = $this->makeNewEmptyModel();
214
        $field->setOption('is_prototype', true);
215
        $field = $this->setupChild($field, $this->getPrototypeName(), $value);
216
217
        if ($field instanceof ChildFormType) {
218
            foreach ($field->getChildren() as $child) {
219
                if ($child instanceof CollectionType) {
220
                    $child->preparePrototype($child->prototype());
221
                }
222
            }
223
        }
224
225
        $this->proto = $field;
226
    }
227
228
    /**
229
     * Generate array like prototype name.
230
     *
231
     * @return string
232
     */
233
    protected function getPrototypeName()
234
    {
235
        return '[' . $this->getOption('prototype_name') . ']';
236
    }
237
238
    /**
239
     * Prepare collection for prototype by adding prototype as child.
240
     *
241
     * @param FormField $field
242
     * @return void
243
     */
244
    public function preparePrototype(FormField $field)
245
    {
246
        if (!$field->getOption('is_prototype')) {
247
            throw new \InvalidArgumentException(
248
                'Field ['.$field->getRealName().'] is not a valid prototype object.'
249
            );
250
        }
251
252
        $this->children = [];
253
        $this->children[] = $field;
254
    }
255
}
256