Completed
Push — dev ( 38ee71...a2773e )
by James Ekow Abaka
49:57 queued 25:07
created

RecordWrapper::onValidate()   A

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
     * A quoted string of the table name used for building queries.
81
     *
82
     * @var string
83
     */
84
    private $quotedTable;
85
86
    /**
87
     * The raw table name without any quotes.
88
     *
89
     * @var string
90
     */
91
    private $unquotedTable;
92
93
    /**
94
     * An array of fields that contain validation errors after an attempted save.
95
     *
96
     * @var array
97
     */
98
    private $invalidFields;
99
100
    /**
101
     * An instance of the operations dispatcher.
102
     *
103
     * @var Operations
104
     */
105
    private $dynamicOperations;
106
107
    /**
108
     * Location of the RecordWrapper's internal iterator.
109
     *
110
     * @var int
111
     */
112
    private $index = 0;
113
114
    /**
115
     * This flag is set whenever data is manually put in the model with the setData method.
116
     *
117
     * @var bool
118
     */
119
    private $dataSet = false;
120
121
    /**
122
     * The name of the class for this model obtained through reflection.
123
     *
124
     * @var string
125
     */
126
    private $className;
127
128
    /**
129
     * An instance of the driver adapter for interacting with the database.
130
     *
131
     * @var DriverAdapter
132
     */
133
    private $adapter;
134
135
    /**
136
     * An instance of the ORMContext through which this model is operating.
137
     *
138
     * @var ORMContext
139
     */
140
    private $context;
141
142
    /**
143
     * Keys for the various fields when model is accessed as an associative array.
144
     *
145
     * @var array
146
     */
147
    private $keys = [];
148
149
    /**
150
     * This flag is set after the model has been properly initialized.
151
     * Useful after model is unserialized or accessed through the static interface.
152
     *
153
     * @var bool
154
     */
155
    private $initialized = false;
156
157
    /**
158
     * Initialize the record wrapper and setup the adapters, drivers, tables and schemas.
159
     * After initialization, this method sets the initialized flag.
160
     *
161
     * @return void
162
     */
163 36
    protected function initialize(): void
164
    {
165 36
        if ($this->initialized) {
166 36
            return;
167
        }
168 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...
169 36
        $this->adapter = $this->context->getDriverAdapter();
170 36
        $table = $this->table ?? $this->context->getModelTable($this);
171 36
        $driver = $this->context->getDbContext()->getDriver();
172 36
        $this->adapter->setContext($this->context);
173 36
        $this->className = (new \ReflectionClass($this))->getName();
174 36
        if (is_string($table)) {
175 36
            $this->table = $this->unquotedTable = $table;
176
        } else {
177
            $this->table = $table['table'];
178
            $this->schema = $table['schema'];
179
        }
180 36
        $this->quotedTable = ($this->schema ? "{$driver->quoteIdentifier($this->schema)}." : '').$driver->quoteIdentifier($this->table);
181 36
        $this->unquotedTable = ($this->schema ? "{$this->schema}." : '').$this->table;
182 36
        $this->adapter->setModel($this, $this->quotedTable);
183 36
        $this->initialized = true;
184 36
    }
185
186
    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...
187
    {
188
        $data = $this->getData();
189
190
        return $this->hasMultipleItems() ? $data : isset($data[0]) ? $data[0] : [];
191
    }
192
193
    /**
194
     * Return a description of the model wrapped by this wrapper.
195
     *
196
     * @return ModelDescription
197
     */
198 28
    public function getDescription() : ModelDescription
199
    {
200 28
        $this->initialize();
201
202 28
        return $this->context->getCache()->read(
203
            "{$this->className}::desc", function () {
204 28
                return $this->context->getModelDescription($this);
205 28
            }
206
        );
207
    }
208
209
    /**
210
     * Return the number of items stored in the model or matched by the query.
211
     * Depending on the state of the model, the count method will return different values. For models that have data
212
     * values set with calls to setData, this method returns the number of records that were added. On the other hand,
213
     * for models that do not have data set, this method queries the database to find out the number of records that
214
     * are either in the model, or for models that have been filtered, the number of records that match the filter.
215
     *
216
     * @param int|array|QueryParameters $query
217
     *
218
     * @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...
219
     */
220 8
    public function count($query = null)
221
    {
222 8
        if ($this->dataSet) {
223 8
            return count($this->getData());
224
        }
225
226
        return $this->__call('count', [$query]);
227
    }
228
229
    /**
230
     * Retrieve an item stored in the record.
231
     * This method returns items that are directly stored in the model, or lazy loads related items if needed.
232
     * The key could be a field in the model's table or the name of a related model.
233
     *
234
     * @param string $key A key identifying the item to be retrieved.
235
     *
236
     * @return mixed
237
     */
238 12
    private function retrieveItem($key)
239
    {
240 12
        if ($this->hasMultipleItems()) {
241
            throw new NibiiException('Current model object state contains multiple items. Please index with a numeric key to select a specific item first.');
242
        }
243 12
        $relationships = $this->getDescription()->getRelationships();
244 12
        $decamelizedKey = Text::deCamelize($key);
245 12
        if (isset($relationships[$key])) {
246 8
            return $this->fetchRelatedFields($relationships[$key]);
247
        }
248
249 4
        return isset($this->modelData[$decamelizedKey]) ? $this->modelData[$decamelizedKey] : null;
250
    }
251
252
    /**
253
     * Calls dynamic methods.
254
     *
255
     * @param string $name
256
     * @param array  $arguments
257
     * @throws exceptions\NibiiException
258
     * @return type
259
     *
260
     */
261
    public function __call($name, $arguments)
262 34
    {
263
        $this->initialize();
264 34
        if ($this->dynamicOperations === null) {
265 34
            $this->dynamicOperations = new Operations($this, $this->quotedTable);
266 34
        }
267
268
        return $this->dynamicOperations->perform($name, $arguments);
269 34
    }
270
271
    /**
272
     * Set a value for a field in the model.
273
     *
274
     * @param string $name
275
     * @param mixed  $value
276
     */
277
    public function __set($name, $value)
278 8
    {
279
        $this->dataSet = true;
280 8
        $this->modelData[Text::deCamelize($name)] = $value;
281 8
    }
282 8
283
    public function __get($name)
284 12
    {
285
        return $this->retrieveItem($name);
286 12
    }
287
288
    public function save()
289 10
    {
290
        $return = $this->__call('save', [$this->hasMultipleItems()]);
291 10
        $this->invalidFields = $this->dynamicOperations->getInvalidFields();
292 10
293
        return $return;
294 10
    }
295
296
    private function hasMultipleItems()
297 26
    {
298
        if (count($this->modelData) > 0) {
299 26
            return is_numeric(array_keys($this->modelData)[0]);
300 24
        } else {
301
            return false;
302 2
        }
303
    }
304
305
    public function getData()
306 18
    {
307
        $data = [];
308 18
309
        if (count($this->modelData) == 0) {
310 18
            $data = $this->modelData;
311 2
        } elseif ($this->hasMultipleItems()) {
312 16
            $data = $this->modelData;
313 6
        } elseif (count($this->modelData) > 0) {
314 12
            $data[] = $this->modelData;
315 12
        }
316
317
        return $data;
318 18
    }
319
320
    public function setData($data)
321 26
    {
322
        $this->dataSet = is_array($data) ? true : false;
323 26
        $this->modelData = $data;
324 26
    }
325 26
326
    public function mergeData($data)
327
    {
328
        foreach ($data as $key => $value) {
329
            $this->modelData[$key] = $value;
330
        }
331
        $this->dataSet = true;
332
    }
333
334
    public function offsetExists($offset)
335 2
    {
336
        return isset($this->modelData[$offset]);
337 2
    }
338
339
    public function offsetGet($offset)
340 2
    {
341
        if (is_numeric($offset)) {
342 2
            return $this->wrap($offset);
343 2
        } else {
344
            return $this->retrieveItem($offset);
345 2
        }
346
    }
347
348
    public function offsetSet($offset, $value)
349 2
    {
350
        $this->dataSet = true;
351 2
        $this->modelData[$offset] = $value;
352 2
    }
353 2
354
    public function offsetUnset($offset)
355
    {
356
        unset($this->modelData[$offset]);
357
    }
358
359
    private function wrap($offset)
360 6
    {
361
        $this->initialize();
362 6
        if (isset($this->modelData[$offset])) {
363 6
            $className = $this->className;
364 6
            $newInstance = new $className();
365 6
            $newInstance->initialize();
366 6
            $newInstance->setData($this->modelData[$offset]);
367 6
368
            return $newInstance;
369 6
        } else {
370
            return;
371
        }
372
    }
373
374
    public function getInvalidFields()
375 4
    {
376
        return $this->invalidFields;
377 4
    }
378
379
    public function getHasMany()
380
    {
381
        return $this->hasMany;
382
    }
383
384
    public function getBelongsTo()
385
    {
386
        return $this->belongsTo;
387
    }
388
389
    public function current()
390 4
    {
391
        return $this->wrap($this->keys[$this->index]);
392 4
    }
393
394
    public function key()
395
    {
396
        return $this->keys[$this->index];
397
    }
398
399
    public function next()
400 4
    {
401
        $this->index++;
402 4
    }
403 4
404
    public function rewind()
405 4
    {
406
        $this->keys = array_keys($this->modelData);
407 4
        $this->index = 0;
408 4
    }
409 4
410
    public function valid()
411 4
    {
412
        return isset($this->keys[$this->index]) && isset($this->modelData[$this->keys[$this->index]]);
413 4
    }
414
415
    /**
416
     * A custom validator for the record wrapper.
417
     *
418
     * @return mixed
419
     */
420
    public function onValidate($invalidFields)
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...
421 10
    {
422
        return [];
423 10
    }
424
425
    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...
426 12
    {
427
        if ($index === null) {
428 12
            $data = $this->modelData;
429 12
        } else {
430
            $data = $this->modelData[$index];
431
        }
432
        $model = $relationship->getModelInstance();
433 12
        if (empty($data)) {
434 12
            return $model;
435
        }
436
        $query = $relationship->prepareQuery($data);
437 12
438
        return $query ? $model->fetch($query) : $model;
439 12
    }
440
441
    public function getRelationships()
442 28
    {
443
        return [
444
            'HasMany'      => $this->hasMany,
445 28
            'BelongsTo'    => $this->belongsTo,
446 28
            'ManyHaveMany' => $this->manyHaveMany,
447 28
        ];
448
    }
449
450
    public function usetField($field)
451
    {
452
        unset($this->modelData[$field]);
453
    }
454
455
    /**
456
     * Callback for when a record is either added or modified.
457
     */
458
    public function preSaveCallback()
459 10
    {
460
    }
461 10
462
    /**
463
     * Callback for when a record has been added or modified.
464
     *
465
     * @param $id
466
     */
467
    public function postSaveCallback()
468 6
    {
469
    }
470 6
471
    /**
472
     * Callback for when a new record is about to be created.
473
     */
474
    public function preCreateCallback()
475 8
    {
476
    }
477 8
478
    /**
479
     * Callback for when a new record has been created.
480
     * This callback can be most useful for obtaining the primary key of a newly created record.
481
     *
482
     * @param $id
483
     */
484
    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...
485 4
    {
486
    }
487 4
488
    /**
489
     * Callback for when a record is about to be updated.
490
     */
491
    public function preUpdateCallback()
492 2
    {
493
    }
494 2
495
    /**
496
     * Callback for when a record has been updated.
497
     */
498
    public function postUpdateCallback()
499 2
    {
500
    }
501 2
502
    public function getDBStoreInformation()
503 36
    {
504
        $this->initialize();
505 36
506
        return [
507
            'schema'         => $this->schema,
508 36
            'table'          => $this->table,
509 36
            'quoted_table'   => $this->quotedTable,
510 36
            'unquoted_table' => $this->unquotedTable,
511 36
        ];
512
    }
513
514
    /**
515
     * @return DriverAdapter
516
     */
517
    public function getAdapter()
518 36
    {
519
        $this->initialize();
520 36
521
        return $this->adapter;
522 36
    }
523
524
    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...
525 4
    {
526
        if (empty($expandableModels)) {
527 4
            foreach ($relationships as $name => $relationship) {
528 4
                $array[$name] = $this->fetchRelatedFields($relationship, $index)->toArray($depth);
529 4
            }
530
        } else {
531
            foreach ($expandableModels as $name) {
532
                $array[$name] = $this->fetchRelatedFields($relationships[$name], $index)->toArray($depth, $expandableModels);
533
            }
534
        }
535
536
        return $array;
537 4
    }
538
539
    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...
540 24
    {
541
        $relationships = $this->getDescription()->getRelationships();
542 24
        $array = $this->modelData;
543 24
        if ($depth > 0) {
544 24
            if ($this->hasMultipleItems()) {
545 4
                foreach ($array as $i => $value) {
546
                    $array[$i] = $this->expandArrayValue($value, $relationships, $depth - 1, $expandableModels, $i);
547
                }
548
            } else {
549
                $array = $this->expandArrayValue($array, $relationships, $depth - 1, $expandableModels);
550 4
            }
551
        }
552
553
        return $array;
554 24
    }
555
}
556