RecordWrapper::onValidate()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
/*
4
 * The MIT License
5
 *
6
 * Copyright 2014-2018 James Ekow Abaka Ainooson
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\nibii\exceptions\NibiiException;
30
use ntentan\utils\Text;
31
32
/**
33
 * An active record wrapper for database records.
34
 */
35
class RecordWrapper implements \ArrayAccess, \Countable, \Iterator
36
{
37
    /**
38
     * An associative array of models to which this model has a one to may relationship.
39
     *
40
     * @var array
41
     */
42
    protected $hasMany = [];
43
44
    /**
45
     * An associative array of models which have a one-to-many relationship with this model.
46
     *
47
     * @var array
48
     */
49
    protected $belongsTo = [];
50
51
    /**
52
     * An associative array of models with which this model has a many to many relationship.
53
     *
54
     * @var array
55
     */
56
    protected $manyHaveMany = [];
57
58
    /**
59
     * The name of the database table.
60
     *
61
     * @var string
62
     */
63
    protected $table;
64
65
    /**
66
     * The name of the schema to which this table belongs.
67
     *
68
     * @var string
69
     */
70
    protected $schema;
71
72
    /**
73
     * Temporary data held in the model object.
74
     *
75
     * @var array
76
     */
77
    protected $modelData = [];
78
79
    /**
80
     * Extra validation rules to use over the model's inherent validation requirements.
81
     * @var array
82
     */
83
    protected $validationRules = [];
84
85
    /**
86
     * A quoted string of the table name used for building queries.
87
     *
88
     * @var string
89
     */
90
    private $quotedTable;
91
92
    /**
93
     * The raw table name without any quotes.
94
     *
95
     * @var string
96
     */
97
    private $unquotedTable;
98
99
    /**
100
     * An array of fields that contain validation errors after an attempted save.
101
     *
102
     * @var array
103
     */
104
    private $invalidFields;
105
106
    /**
107
     * An instance of the operations dispatcher.
108
     *
109
     * @var Operations
110
     */
111
    private $dynamicOperations;
112
113
    /**
114
     * Location of the RecordWrapper's internal iterator.
115
     *
116
     * @var int
117
     */
118
    private $index = 0;
119
120
    /**
121
     * This flag is set whenever data is manually put in the model with the setData method.
122
     *
123
     * @var bool
124
     */
125
    private $dataSet = false;
126
127
    /**
128
     * The name of the class for this model obtained through reflection.
129
     *
130
     * @var string
131
     */
132
    private $className;
133
134
    /**
135
     * An instance of the driver adapter for interacting with the database.
136
     *
137
     * @var DriverAdapter
138
     */
139
    private $adapter;
140
141
    /**
142
     * An instance of the ORMContext through which this model is operating.
143
     *
144
     * @var ORMContext
145
     */
146
    private $context;
147
148
    /**
149
     * Keys for the various fields when model is accessed as an associative array.
150
     *
151
     * @var array
152
     */
153
    private $keys = [];
154
155
    /**
156
     * This flag is set after the model has been properly initialized.
157
     * Useful after model is unserialized or accessed through the static interface.
158
     *
159
     * @var bool
160
     */
161
    private $initialized = false;
162
163
    /**
164
     * Initialize the record wrapper and setup the adapters, drivers, tables and schemas.
165
     * After initialization, this method sets the initialized flag.
166
     *
167
     * @return void
168
     */
169 36
    protected function initialize(): void
170
    {
171 36
        if ($this->initialized) {
172 36
            return;
173
        }
174 36
        $this->context = ORMContext::getInstance();
0 ignored issues
show
Documentation Bug introduced by
It seems like \ntentan\nibii\ORMContext::getInstance() of type object<self> is incompatible with the declared type object<ntentan\nibii\ORMContext> of property $context.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
175 36
        $this->adapter = $this->context->getDriverAdapter();
176 36
        $table = $this->table ?? $this->context->getModelTable($this);
177 36
        $driver = $this->context->getDbContext()->getDriver();
178 36
        $this->adapter->setContext($this->context);
179 36
        $this->className = (new \ReflectionClass($this))->getName();
180 36
        if (is_string($table)) {
181 36
            $this->table = $this->unquotedTable = $table;
182
        } else {
183
            $this->table = $table['table'];
184
            $this->schema = $table['schema'];
185
        }
186 36
        $this->quotedTable = ($this->schema ? "{$driver->quoteIdentifier($this->schema)}." : '').$driver->quoteIdentifier($this->table);
187 36
        $this->unquotedTable = ($this->schema ? "{$this->schema}." : '').$this->table;
188 36
        $this->adapter->setModel($this, $this->quotedTable);
189 36
        $this->initialized = true;
190 36
    }
191
192
    public function __debugInfo()
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
193
    {
194
        $data = $this->getData();
195
196
        return $this->hasMultipleItems() ? $data : isset($data[0]) ? $data[0] : [];
197
    }
198
199
    /**
200
     * Return a description of the model wrapped by this wrapper.
201
     *
202
     * @return ModelDescription
203
     */
204 28
    public function getDescription() : ModelDescription
205
    {
206 28
        $this->initialize();
207
208 28
        return $this->context->getCache()->read(
209
            "{$this->className}::desc", function () {
210 28
                return $this->context->getModelDescription($this);
211 28
            }
212
        );
213
    }
214
215
    /**
216
     * Return the number of items stored in the model or matched by the query.
217
     * Depending on the state of the model, the count method will return different values. For models that have data
218
     * values set with calls to setData, this method returns the number of records that were added. On the other hand,
219
     * for models that do not have data set, this method queries the database to find out the number of records that
220
     * are either in the model, or for models that have been filtered, the number of records that match the filter.
221
     *
222
     * @param int|array|QueryParameters $query
223
     *
224
     * @return int
0 ignored issues
show
Documentation introduced by
Should the return type not be integer|type?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
225
     */
226 8
    public function count($query = null)
227
    {
228 8
        if ($this->dataSet) {
229 8
            return count($this->getData());
230
        }
231
232
        return $this->__call('count', [$query]);
233
    }
234
235
    /**
236
     * Retrieve an item stored in the record.
237
     * This method returns items that are directly stored in the model, or lazy loads related items if needed.
238
     * The key could be a field in the model's table or the name of a related model.
239
     *
240
     * @param string $key A key identifying the item to be retrieved.
241
     *
242
     * @return mixed
243
     */
244 12
    private function retrieveItem($key)
245
    {
246 12
        if ($this->hasMultipleItems()) {
247
            throw new NibiiException('Current model object state contains multiple items. Please index with a numeric key to select a specific item first.');
248
        }
249 12
        $relationships = $this->getDescription()->getRelationships();
250 12
        $decamelizedKey = Text::deCamelize($key);
251 12
        if (isset($relationships[$key])) {
252 8
            return $this->fetchRelatedFields($relationships[$key]);
253
        }
254
255 4
        return isset($this->modelData[$decamelizedKey]) ? $this->modelData[$decamelizedKey] : null;
256
    }
257
258
    /**
259
     * Calls dynamic methods.
260
     *
261
     * @param string $name
262
     * @param array  $arguments
263
     *
264
     * @throws exceptions\NibiiException
265
     *
266
     * @return type
267
     */
268 34
    public function __call($name, $arguments)
269
    {
270 34
        $this->initialize();
271 34
        if ($this->dynamicOperations === null) {
272 34
            $this->dynamicOperations = new Operations($this, $this->quotedTable);
273
        }
274
275 34
        return $this->dynamicOperations->perform($name, $arguments);
276
    }
277
278
    /**
279
     * Set a value for a field in the model.
280
     *
281
     * @param string $name
282
     * @param mixed  $value
283
     */
284 8
    public function __set($name, $value)
285
    {
286 8
        $this->dataSet = true;
287 8
        $this->modelData[Text::deCamelize($name)] = $value;
288 8
    }
289
290 12
    public function __get($name)
291
    {
292 12
        return $this->retrieveItem($name);
293
    }
294
295 10
    public function save()
296
    {
297 10
        $return = $this->__call('save', [$this->hasMultipleItems()]);
298 10
        $this->invalidFields = $this->dynamicOperations->getInvalidFields();
299
300 10
        return $return;
301
    }
302
303 26
    private function hasMultipleItems()
304
    {
305 26
        if (count($this->modelData) > 0) {
306 24
            return is_numeric(array_keys($this->modelData)[0]);
307
        } else {
308 2
            return false;
309
        }
310
    }
311
312 18
    public function getData()
313
    {
314 18
        $data = [];
315
316 18
        if (count($this->modelData) == 0) {
317 2
            $data = $this->modelData;
318 16
        } elseif ($this->hasMultipleItems()) {
319 6
            $data = $this->modelData;
320 12
        } elseif (count($this->modelData) > 0) {
321 12
            $data[] = $this->modelData;
322
        }
323
324 18
        return $data;
325
    }
326
327 26
    public function setData($data)
328
    {
329 26
        $this->dataSet = is_array($data) ? true : false;
330 26
        $this->modelData = $data;
331 26
    }
332
333
    public function mergeData($data)
334
    {
335
        foreach ($data as $key => $value) {
336
            $this->modelData[$key] = $value;
337
        }
338
        $this->dataSet = true;
339
    }
340
341 2
    public function offsetExists($offset)
342
    {
343 2
        return isset($this->modelData[$offset]);
344
    }
345
346 2
    public function offsetGet($offset)
347
    {
348 2
        if (is_numeric($offset)) {
349 2
            return $this->wrap($offset);
350
        } else {
351 2
            return $this->retrieveItem($offset);
352
        }
353
    }
354
355 2
    public function offsetSet($offset, $value)
356
    {
357 2
        $this->dataSet = true;
358 2
        $this->modelData[$offset] = $value;
359 2
    }
360
361
    public function offsetUnset($offset)
362
    {
363
        unset($this->modelData[$offset]);
364
    }
365
366 6
    private function wrap($offset)
367
    {
368 6
        $this->initialize();
369 6
        if (isset($this->modelData[$offset])) {
370 6
            $className = $this->className;
371 6
            $newInstance = new $className();
372 6
            $newInstance->initialize();
373 6
            $newInstance->setData($this->modelData[$offset]);
374
375 6
            return $newInstance;
376
        } else {
377
            return;
378
        }
379
    }
380
381 4
    public function getInvalidFields()
382
    {
383 4
        return $this->invalidFields;
384
    }
385
386
    public function getHasMany()
387
    {
388
        return $this->hasMany;
389
    }
390
391
    public function getBelongsTo()
392
    {
393
        return $this->belongsTo;
394
    }
395
396 4
    public function current()
397
    {
398 4
        return $this->wrap($this->keys[$this->index]);
399
    }
400
401
    public function key()
402
    {
403
        return $this->keys[$this->index];
404
    }
405
406 4
    public function next()
407
    {
408 4
        $this->index++;
409 4
    }
410
411 4
    public function rewind()
412
    {
413 4
        $this->keys = array_keys($this->modelData);
414 4
        $this->index = 0;
415 4
    }
416
417 4
    public function valid()
418
    {
419 4
        return isset($this->keys[$this->index]) && isset($this->modelData[$this->keys[$this->index]]);
420
    }
421
422
    /**
423
     * A custom validator for the record wrapper.
424
     *
425
     * @return mixed
426
     */
427 10
    public function onValidate($invalidFields) : array 
0 ignored issues
show
Unused Code introduced by
The parameter $invalidFields is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
428
    {
429 10
        return [];
430
    }
431
432 12
    private function fetchRelatedFields($relationship, $index = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
433
    {
434 12
        if ($index === null) {
435 12
            $data = $this->modelData;
436
        } else {
437
            $data = $this->modelData[$index];
438
        }
439 12
        $model = $relationship->getModelInstance();
440 12
        if (empty($data)) {
441
            return $model;
442
        }
443 12
        $query = $relationship->prepareQuery($data);
444
445 12
        return $query ? $model->fetch($query) : $model;
446
    }
447
448 28
    public function getRelationships()
449
    {
450
        return [
451 28
            'HasMany'      => $this->hasMany,
452 28
            'BelongsTo'    => $this->belongsTo,
453 28
            'ManyHaveMany' => $this->manyHaveMany,
454
        ];
455
    }
456
457
    public function usetField($field)
458
    {
459
        unset($this->modelData[$field]);
460
    }
461
462
    /**
463
     * Callback for when a record is either added or modified.
464
     */
465 10
    public function preSaveCallback()
466
    {
467 10
    }
468
469
    /**
470
     * Callback for when a record has been added or modified.
471
     *
472
     * @param $id
473
     */
474 6
    public function postSaveCallback()
475
    {
476 6
    }
477
478
    /**
479
     * Callback for when a new record is about to be created.
480
     */
481 8
    public function preCreateCallback()
482
    {
483 8
    }
484
485
    /**
486
     * Callback for when a new record has been created.
487
     * This callback can be most useful for obtaining the primary key of a newly created record.
488
     *
489
     * @param $id
490
     */
491 4
    public function postCreateCallback($id)
0 ignored issues
show
Unused Code introduced by
The parameter $id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
492
    {
493 4
    }
494
495
    /**
496
     * Callback for when a record is about to be updated.
497
     */
498 2
    public function preUpdateCallback()
499
    {
500 2
    }
501
502
    /**
503
     * Callback for when a record has been updated.
504
     */
505 2
    public function postUpdateCallback()
506
    {
507 2
    }
508
509 36
    public function getDBStoreInformation()
510
    {
511 36
        $this->initialize();
512
513
        return [
514 36
            'schema'         => $this->schema,
515 36
            'table'          => $this->table,
516 36
            'quoted_table'   => $this->quotedTable,
517 36
            'unquoted_table' => $this->unquotedTable,
518
        ];
519
    }
520
521
    /**
522
     * @return DriverAdapter
523
     */
524 36
    public function getAdapter()
525
    {
526 36
        $this->initialize();
527 36
        return $this->adapter;
528
    }
529
530 4
    private function expandArrayValue($array, $relationships, $depth, $expandableModels = [], $index = null)
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
531
    {
532 4
        if (empty($expandableModels)) {
533 4
            foreach ($relationships as $name => $relationship) {
534 4
                $array[$name] = $this->fetchRelatedFields($relationship, $index)->toArray($depth);
535
            }
536
        } else {
537
            foreach ($expandableModels as $name) {
538
                $array[$name] = $this->fetchRelatedFields($relationships[$name], $index)->toArray($depth, $expandableModels);
539
            }
540
        }
541
542 4
        return $array;
543
    }
544
545 10
    public function getValidationRules() : array
546
    {
547 10
        return $this->validationRules;
548
    }
549
550 24
    public function toArray($depth = 0, $expandableModels = [])
0 ignored issues
show
Documentation introduced by
The return type could not be reliably inferred; please add a @return annotation.

Our type inference engine in quite powerful, but sometimes the code does not provide enough clues to go by. In these cases we request you to add a @return annotation as described here.

Loading history...
551
    {
552 24
        $relationships = $this->getDescription()->getRelationships();
553 24
        $array = $this->modelData;
554 24
        if ($depth > 0) {
555 4
            if ($this->hasMultipleItems()) {
556
                foreach ($array as $i => $value) {
557
                    $array[$i] = $this->expandArrayValue($value, $relationships, $depth - 1, $expandableModels, $i);
558
                }
559
            } else {
560 4
                $array = $this->expandArrayValue($array, $relationships, $depth - 1, $expandableModels);
561
            }
562
        }
563
564 24
        return $array;
565
    }
566
}
567