Completed
Push — master ( bef35d...711c39 )
by James Ekow Abaka
01:36
created

DataOperations   B

Complexity

Total Complexity 36

Size/Duplication

Total Lines 275
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Test Coverage

Coverage 83.87%

Importance

Changes 7
Bugs 0 Features 0
Metric Value
wmc 36
c 7
b 0
f 0
lcom 1
cbo 6
dl 0
loc 275
ccs 78
cts 93
cp 0.8387
rs 8.8

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B doSave() 0 38 6
A doValidate() 0 7 2
C saveRecord() 0 68 10
A validate() 0 11 3
B isPrimaryKeySet() 0 12 8
A assignValue() 0 8 2
A getData() 0 4 1
A getInvalidFields() 0 4 1
A isItemDeletable() 0 8 2
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2015 ekow.
7
 *
8
 * Permission is hereby granted, free of charge, to any person obtaining a copy
9
 * of this software and associated documentation files (the "Software"), to deal
10
 * in the Software without restriction, including without limitation the rights
11
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12
 * copies of the Software, and to permit persons to whom the Software is
13
 * furnished to do so, subject to the following conditions:
14
 *
15
 * The above copyright notice and this permission notice shall be included in
16
 * all copies or substantial portions of the Software.
17
 *
18
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
24
 * THE SOFTWARE.
25
 */
26
27
namespace ntentan\nibii;
28
29
use ntentan\atiaa\Driver;
30
31
/**
32
 * Description of DataOperations
33
 *
34
 * @author ekow
35
 */
36
class DataOperations
37
{
38
39
    private $wrapper;
40
41
    /**
42
     * Private instance of driver adapter
43
     *
44
     * @var DriverAdapter
45
     */
46
    private $adapter;
47
48
    /**
49
     * Copy of data to be manipulated in the operations.
50
     * @var array
51
     */
52
    private $data;
53
54
    /**
55
     * Fields that contained errors after save or update operations were performed.
56
     * @var array
57
     */
58
    private $invalidFields = [];
59
60
    /**
61
     * Set to true when the model holds multiple records.
62
     * @var bool
63
     */
64
    private $hasMultipleData;
65
66
    /**
67
     * An instance of the atiaa driver.
68
     * @var Driver
69
     */
70
    private $driver;
71
72
    /**
73
     * Used to indicate save operation is in save mode to create new items.
74
     */
75
    const MODE_SAVE = 0;
76
    
77
    /**
78
     * Used to indicate save operation is in update mode to update existing items.
79
     */
80
    const MODE_UPDATE = 1;
81
82
    /**
83
     * 
84
     * @param \ntentan\nibii\RecordWrapper $wrapper
85
     * @param Driver $driver
86
     */
87 34
    public function __construct(RecordWrapper $wrapper, Driver $driver)
88
    {
89 34
        $this->wrapper = $wrapper;
90 34
        $this->adapter = $wrapper->getAdapter();
91 34
        $this->driver = $driver;
92 34
    }
93
94
    /**
95
     * 
96
     * @param bool $hasMultipleData
97
     * @return bool
98
     */
99 10
    public function doSave(bool $hasMultipleData): bool
100
    {
101 10
        $this->hasMultipleData = $hasMultipleData;
102 10
        $invalidFields = [];
103 10
        $data = $this->wrapper->getData();
104
105 10
        $primaryKey = $this->wrapper->getDescription()->getPrimaryKey();
106 10
        $succesful = true;
107
108
        // Assign an empty array to force a validation error for empty models
109 10
        if (empty($data)) {
110 2
            $data = [[]];
111
        }
112
113 10
        $this->driver->beginTransaction();
114
115 10
        foreach ($data as $i => $datum) {
116 10
            $status = $this->saveRecord($datum, $primaryKey);
117 10
            $data[$i] = $datum;
118
119 10
            if (!$status['success']) {
120 4
                $succesful = false;
121 4
                $invalidFields[$i] = $status['invalid_fields'];
122 4
                $this->driver->rollback();
123 10
                break;
124
            }
125
        }
126
127 10
        if ($succesful) {
128 6
            $this->driver->commit();
129
        } else {
130 4
            $this->assignValue($this->invalidFields, $invalidFields);
131
        }
132
133 10
        $this->wrapper->setData($hasMultipleData ? $data : $data[0]);
134
135 10
        return $succesful;
136
    }
137
138
    /**
139
     * 
140
     * @return bool|array
141
     */
142
    public function doValidate()
143
    {
144
        $record = $this->wrapper->getData()[0];
145
        $primaryKey = $this->wrapper->getDescription()->getPrimaryKey();
146
        $pkSet = $this->isPrimaryKeySet($primaryKey, $record);
147
        return $this->validate($record, $pkSet ? DataOperations::MODE_UPDATE : DataOperations::MODE_SAVE);
148
    }
149
150
    /**
151
     * Save an individual record.
152
     * 
153
     * @param array $record The record to be saved
154
     * @param array $primaryKey The primary keys of the record
155
     * @return array
156
     */
157 10
    private function saveRecord(array &$record, array $primaryKey): array
158
    {
159
        $status = [
160 10
            'success' => true,
161
            'pk_assigned' => null,
162
            'invalid_fields' => []
163
        ];
164
165
        // Determine if the primary key of the record is set.
166 10
        $pkSet = $this->isPrimaryKeySet($primaryKey, $record);
167
168
        // Reset the data in the model to contain only the data to be saved
169 10
        $this->wrapper->setData($record);
170
171
        // Run preUpdate or preSave callbacks on models and behaviours
172 10
        if ($pkSet) {
173 2
            $this->wrapper->preUpdateCallback();
174 2
            $record = $this->wrapper->getData();
175 2
            $record = reset($record) === false ? [] : reset($record);
176
        } else {
177 8
            $this->wrapper->preSaveCallback();
178 8
            $record = $this->wrapper->getData();
179 8
            $record = reset($record) === false ? [] : reset($record);
180
        }
181
182
        // Validate the data
183 10
        $validity = $this->validate($record, $pkSet ? DataOperations::MODE_UPDATE : DataOperations::MODE_SAVE);
184
185
        // Exit if data is invalid
186 10
        if ($validity !== true) {
187 4
            $status['invalid_fields'] = $validity;
188 4
            $status['success'] = false;
189 4
            return $status;
190
        }
191
192
        // Save any relationships that are attached to the data
193 6
        $relationships = $this->wrapper->getDescription()->getRelationships();
194 6
        $presentRelationships = [];
195
196 6
        foreach ($relationships ?? [] as $model => $relationship) {
197 6
            if (isset($record[$model])) {
198
                $relationship->preSave($record, $record[$model]);
199 6
                $presentRelationships[$model] = $relationship;
200
            }
201
        }
202
203
        // Assign the data to the wrapper again
204 6
        $this->wrapper->setData($record);
205
206
        // Update or save the data and run post callbacks
207 6
        if ($pkSet) {
208 2
            $this->adapter->update($record);
209 2
            $this->wrapper->postUpdateCallback();
210
        } else {
211 4
            $this->adapter->insert($record);
212 4
            $keyValue = $this->driver->getLastInsertId();
213 4
            $this->wrapper->{$primaryKey[0]} = $keyValue;
214 4
            $this->wrapper->postSaveCallback($keyValue);
215
        }
216
217
        // Reset the data so it contains any modifications made by callbacks
218 6
        $record = $this->wrapper->getData()[0];
219 6
        foreach ($presentRelationships as $model => $relationship) {
220
            $relationship->postSave($record);
221
        }
222
223 6
        return $status;
224
    }
225
226
    /**
227
     * 
228
     * @param array $data
229
     * @param type $mode
230
     * @return bool|array
231
     */
232 10
    private function validate(array $data, int $mode)
233
    {
234 10
        $validator = ORMContext::getInstance()->getModelValidatorFactory()->createModelValidator($this->wrapper, $mode);
235 10
        $errors = [];
236
237 10
        if (!$validator->validate($data)) {
238 4
            $errors = $validator->getInvalidFields();
239
        }
240 10
        $errors = $this->wrapper->onValidate($errors);
241 10
        return empty($errors) ? true : $errors;
242
    }
243
244
    /**
245
     * 
246
     * @param string|array $primaryKey
247
     * @param array $data
248
     * @return bool
249
     */
250 10
    private function isPrimaryKeySet($primaryKey, array $data) : bool
251
    {
252 10
        if (is_string($primaryKey) && ($data[$primaryKey] !== null || $data[$primaryKey] !== '')) {
253
            return true;
254
        }
255 10
        foreach ($primaryKey as $keyField) {
1 ignored issue
show
Bug introduced by
The expression $primaryKey of type array|string is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
256 10
            if (!isset($data[$keyField]) || $data[$keyField] === null || $data[$keyField] === '') {
257 10
                return false;
258
            }
259
        }
260 2
        return true;
261
    }
262
    
263
    /**
264
     * 
265
     * @param mixed $property
266
     * @param mixed $value
267
     */
268 4
    private function assignValue(&$property, $value) : void
269
    {
270 4
        if ($this->hasMultipleData) {
271
            $property = $value;
272
        } else {
273 4
            $property = $value[0];
274
        }
275 4
    }
276
277
    /**
278
     * 
279
     * @return array
280
     */
281
    public function getData() : array
282
    {
283
        return $this->data;
284
    }
285
286
    /**
287
     * 
288
     * @return array
289
     */
290 10
    public function getInvalidFields() : array
291
    {
292 10
        return $this->invalidFields;
293
    }
294
295
    /**
296
     * 
297
     * @param string $primaryKey
298
     * @param array $data
299
     * @return bool
300
     */
301
    public function isItemDeletable(string $primaryKey, array $data) : bool
302
    {
303
        if ($this->isPrimaryKeySet($primaryKey, $data)) {
304
            return true;
305
        } else {
306
            return false;
307
        }
308
    }
309
310
}
311