Completed
Push — dev ( f292cf...1456c7 )
by James Ekow Abaka
01:41
created

RecordWrapper   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 544
Duplicated Lines 0 %

Coupling/Cohesion

Components 2
Dependencies 7

Test Coverage

Coverage 83.14%

Importance

Changes 0
Metric Value
wmc 72
lcom 2
cbo 7
dl 0
loc 544
c 0
b 0
f 0
ccs 143
cts 172
cp 0.8314
rs 2.64

41 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 22 5
A getRelationships() 0 8 1
A usetField() 0 4 1
A preSaveCallback() 0 3 1
A postSaveCallback() 0 3 1
A preCreateCallback() 0 3 1
A postCreateCallback() 0 3 1
A preUpdateCallback() 0 3 1
A postUpdateCallback() 0 3 1
A getDBStoreInformation() 0 11 1
A getAdapter() 0 5 1
A __debugInfo() 0 5 3
A getDescription() 0 10 1
A count() 0 8 2
A retrieveItem() 0 13 5
A __call() 0 9 2
A __set() 0 5 1
A __get() 0 4 1
A save() 0 7 1
A hasMultipleItems() 0 8 2
A getData() 0 14 4
A setData() 0 5 2
A mergeData() 0 7 2
A offsetExists() 0 4 1
A offsetGet() 0 8 2
A offsetSet() 0 5 1
A offsetUnset() 0 4 1
A wrap() 0 14 2
A getInvalidFields() 0 4 1
A getHasMany() 0 4 1
A getBelongsTo() 0 4 1
A current() 0 4 1
A key() 0 4 1
A next() 0 4 1
A rewind() 0 5 1
A valid() 0 4 2
A onValidate() 0 4 1
A fetchRelatedFields() 0 16 5
A expandArrayValue() 0 15 3
A getValidationRules() 0 4 1
A toArray() 0 16 5

How to fix   Complexity   

Complex Class

Complex classes like RecordWrapper often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use RecordWrapper, and based on these observations, apply Extract Interface, too.

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
     * @throws NibiiException
169
     * @throws \ReflectionException
170
     * @throws \ntentan\atiaa\exceptions\ConnectionException
171
     */
172 32
    protected function initialize(): void
173
    {
174 32
        if ($this->initialized) {
175 32
            return;
176
        }
177 32
        $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...
178 32
        $this->adapter = $this->context->getDriverAdapter();
179 32
        $table = $this->table ?? $this->context->getModelTable($this);
180 32
        $driver = $this->context->getDbContext()->getDriver();
181 32
        $this->adapter->setContext($this->context);
182 32
        $this->className = (new \ReflectionClass($this))->getName();
183 32
        if (is_string($table)) {
184 32
            $this->table = $this->unquotedTable = $table;
185
        } else {
186
            $this->table = $table['table'];
187
            $this->schema = $table['schema'];
188
        }
189 32
        $this->quotedTable = ($this->schema ? "{$driver->quoteIdentifier($this->schema)}." : '').$driver->quoteIdentifier($this->table);
190 32
        $this->unquotedTable = ($this->schema ? "{$this->schema}." : '').$this->table;
191 32
        $this->adapter->setModel($this, $this->quotedTable);
192 32
        $this->initialized = true;
193 32
    }
194
195
    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...
196
    {
197
        $data = $this->getData();
198
        return $this->hasMultipleItems() ? $data : isset($data[0]) ? $data[0] : [];
199
    }
200
201
    /**
202
     * Return a description of the model wrapped by this wrapper.
203
     *
204
     * @return ModelDescription
205
     * @throws NibiiException
206
     * @throws \ReflectionException
207
     * @throws \ntentan\atiaa\exceptions\ConnectionException
208
     */
209 24
    public function getDescription() : ModelDescription
210
    {
211 24
        $this->initialize();
212
213 24
        return $this->context->getCache()->read(
214
            "{$this->className}::desc", function () {
215 24
                return $this->context->getModelDescription($this);
216 24
            }
217
        );
218
    }
219
220
    /**
221
     * Return the number of items stored in the model or matched by the query.
222
     * Depending on the state of the model, the count method will return different values. For models that have data
223
     * values set with calls to setData, this method returns the number of records that were added. On the other hand,
224
     * for models that do not have data set, this method queries the database to find out the number of records that
225
     * are either in the model, or for models that have been filtered, the number of records that match the filter.
226
     *
227
     * @param int|array|QueryParameters $query
228
     *
229
     * @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...
230
     * @throws NibiiException
231
     */
232 8
    public function count($query = null)
233
    {
234 8
        if ($this->dataSet) {
235 8
            return count($this->getData());
236
        }
237
238
        return $this->__call('count', [$query]);
239
    }
240
241
    /**
242
     * Retrieve an item stored in the record.
243
     * This method returns items that are directly stored in the model, or lazy loads related items if needed.
244
     * The key could be a field in the model's table or the name of a related model.
245
     *
246
     * @param string $key A key identifying the item to be retrieved.
247
     *
248
     * @return mixed
249
     * @throws NibiiException
250
     * @throws \ReflectionException
251
     * @throws \ntentan\atiaa\exceptions\ConnectionException
252
     */
253 8
    private function retrieveItem($key)
254
    {
255 8
        if ($this->hasMultipleItems()) {
256
            throw new NibiiException('Current model object state contains multiple items. Please index with a numeric key to select a specific item first.');
257
        }
258 8
        $relationships = $this->getDescription()->getRelationships();
259 8
        $decamelizedKey = Text::deCamelize($key);
260 8
        if (isset($relationships[$decamelizedKey]) && !isset($this->modelData[$decamelizedKey])) {
261 4
            return $this->fetchRelatedFields($relationships[$decamelizedKey]);
262
        }
263
264 4
        return isset($this->modelData[$decamelizedKey]) ? $this->modelData[$decamelizedKey] : null;
265
    }
266
267
    /**
268
     * Calls dynamic methods.
269
     *
270
     * @param string $name
271
     * @param array $arguments
272
     *
273
     * @return type
274
     * @throws NibiiException
275
     * @throws \ReflectionException
276
     * @throws \ntentan\atiaa\exceptions\ConnectionException
277
     */
278 30
    public function __call($name, $arguments)
279
    {
280 30
        $this->initialize();
281 30
        if ($this->dynamicOperations === null) {
282 30
            $this->dynamicOperations = new Operations($this, $this->quotedTable);
283
        }
284
285 30
        return $this->dynamicOperations->perform($name, $arguments);
286
    }
287
288
    /**
289
     * Set a value for a field in the model.
290
     *
291
     * @param string $name
292
     * @param mixed  $value
293
     */
294 8
    public function __set($name, $value)
295
    {
296 8
        $this->dataSet = true;
297 8
        $this->modelData[Text::deCamelize($name)] = $value;
298 8
    }
299
300 8
    public function __get($name)
301
    {
302 8
        return $this->retrieveItem($name);
303
    }
304
305 10
    public function save()
306
    {
307 10
        $return = $this->__call('save', [$this->hasMultipleItems()]);
308 10
        $this->invalidFields = $this->dynamicOperations->getInvalidFields();
309
310 10
        return $return;
311
    }
312
313 22
    private function hasMultipleItems()
314
    {
315 22
        if (count($this->modelData) > 0) {
316 20
            return is_numeric(array_keys($this->modelData)[0]);
317
        } else {
318 2
            return false;
319
        }
320
    }
321
322 18
    public function getData()
323
    {
324 18
        $data = [];
325
326 18
        if (count($this->modelData) == 0) {
327 2
            $data = $this->modelData;
328 16
        } elseif ($this->hasMultipleItems()) {
329 6
            $data = $this->modelData;
330 12
        } elseif (count($this->modelData) > 0) {
331 12
            $data[] = $this->modelData;
332
        }
333
334 18
        return $data;
335
    }
336
337 22
    public function setData($data)
338
    {
339 22
        $this->dataSet = is_array($data) ? true : false;
340 22
        $this->modelData = $data;
341 22
    }
342
343
    public function mergeData($data)
344
    {
345
        foreach ($data as $key => $value) {
346
            $this->modelData[$key] = $value;
347
        }
348
        $this->dataSet = true;
349
    }
350
351 2
    public function offsetExists($offset)
352
    {
353 2
        return isset($this->modelData[$offset]);
354
    }
355
356 2
    public function offsetGet($offset)
357
    {
358 2
        if (is_numeric($offset)) {
359 2
            return $this->wrap($offset);
360
        } else {
361 2
            return $this->retrieveItem($offset);
362
        }
363
    }
364
365 2
    public function offsetSet($offset, $value)
366
    {
367 2
        $this->dataSet = true;
368 2
        $this->modelData[$offset] = $value;
369 2
    }
370
371
    public function offsetUnset($offset)
372
    {
373
        unset($this->modelData[$offset]);
374
    }
375
376 4
    private function wrap($offset)
377
    {
378 4
        $this->initialize();
379 4
        if (isset($this->modelData[$offset])) {
380 4
            $className = $this->className;
381 4
            $newInstance = new $className();
382 4
            $newInstance->initialize();
383 4
            $newInstance->setData($this->modelData[$offset]);
384
385 4
            return $newInstance;
386
        } else {
387
            return;
388
        }
389
    }
390
391 4
    public function getInvalidFields()
392
    {
393 4
        return $this->invalidFields;
394
    }
395
396
    public function getHasMany()
397
    {
398
        return $this->hasMany;
399
    }
400
401
    public function getBelongsTo()
402
    {
403
        return $this->belongsTo;
404
    }
405
406 2
    public function current()
407
    {
408 2
        return $this->wrap($this->keys[$this->index]);
409
    }
410
411
    public function key()
412
    {
413
        return $this->keys[$this->index];
414
    }
415
416 2
    public function next()
417
    {
418 2
        $this->index++;
419 2
    }
420
421 2
    public function rewind()
422
    {
423 2
        $this->keys = array_keys($this->modelData);
424 2
        $this->index = 0;
425 2
    }
426
427 2
    public function valid()
428
    {
429 2
        return isset($this->keys[$this->index]) && isset($this->modelData[$this->keys[$this->index]]);
430
    }
431
432
    /**
433
     * A custom validator for the record wrapper.
434
     *
435
     * @return mixed
436
     */
437 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...
438
    {
439 10
        return [];
440
    }
441
442 8
    private function fetchRelatedFields(Relationship $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...
443
    {
444 8
        $data = $index ? $this->modelData[$index] : $this->modelData;
445 8
        $name = $relationship->getOptions()['name'];
446 8
        if(isset($data[$name]))
447
        {
448
            return $data[$name];
449
        }
450 8
        $model = $relationship->getModelInstance();
451 8
        if (empty($data)) {
452
            return $model;
453
        }
454 8
        $query = $relationship->prepareQuery($data);
455
456 8
        return $query ? $model->fetch($query) : $model;
0 ignored issues
show
Documentation Bug introduced by
The method fetch does not exist on object<ntentan\nibii\RecordWrapper>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
457
    }
458
459 24
    public function getRelationships()
460
    {
461
        return [
462 24
            'HasMany'      => $this->hasMany,
463 24
            'BelongsTo'    => $this->belongsTo,
464 24
            'ManyHaveMany' => $this->manyHaveMany,
465
        ];
466
    }
467
468
    public function usetField($field)
469
    {
470
        unset($this->modelData[$field]);
471
    }
472
473
    /**
474
     * Callback for when a record is either added or modified.
475
     */
476 10
    public function preSaveCallback()
477
    {
478 10
    }
479
480
    /**
481
     * Callback for when a record has been added or modified.
482
     *
483
     * @param $id
484
     */
485 6
    public function postSaveCallback()
486
    {
487 6
    }
488
489
    /**
490
     * Callback for when a new record is about to be created.
491
     */
492 8
    public function preCreateCallback()
493
    {
494 8
    }
495
496
    /**
497
     * Callback for when a new record has been created.
498
     * This callback can be most useful for obtaining the primary key of a newly created record.
499
     *
500
     * @param $id
501
     */
502 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...
503
    {
504 4
    }
505
506
    /**
507
     * Callback for when a record is about to be updated.
508
     */
509 2
    public function preUpdateCallback()
510
    {
511 2
    }
512
513
    /**
514
     * Callback for when a record has been updated.
515
     */
516 2
    public function postUpdateCallback()
517
    {
518 2
    }
519
520 32
    public function getDBStoreInformation()
521
    {
522 32
        $this->initialize();
523
524
        return [
525 32
            'schema'         => $this->schema,
526 32
            'table'          => $this->table,
527 32
            'quoted_table'   => $this->quotedTable,
528 32
            'unquoted_table' => $this->unquotedTable,
529
        ];
530
    }
531
532
    /**
533
     * @return DriverAdapter
534
     */
535 32
    public function getAdapter()
536
    {
537 32
        $this->initialize();
538 32
        return $this->adapter;
539
    }
540
541 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...
542
    {
543 4
        $expandableModels = empty($expandableModels) ? array_keys($relationships) : $expandableModels;
544
//        if (empty($expandableModels)) {
545
//            foreach ($relationships as $name => $relationship) {
546
//                $array[$name] = $this->fetchRelatedFields($relationship, $index)->toArray($depth);
547
//            }
548
//        } else {
549 4
            foreach ($expandableModels as $name) {
550 4
                $array[$name] = $this->fetchRelatedFields($relationships[$name], $index)->toArray($depth, $expandableModels);
551
            }
552
//        }
553
554 4
        return $array;
555
    }
556
557 10
    public function getValidationRules() : array
558
    {
559 10
        return $this->validationRules;
560
    }
561
562 20
    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...
563
    {
564 20
        $relationships = $this->getDescription()->getRelationships();
565 20
        $array = $this->modelData;
566 20
        if (!empty($array) && $depth > 0) {
567 4
            if ($this->hasMultipleItems()) {
568
                foreach ($array as $i => $value) {
569
                    $array[$i] = $this->expandArrayValue($value, $relationships, $depth - 1, $expandableModels, $i);
570
                }
571
            } else {
572 4
                $array = $this->expandArrayValue($array, $relationships, $depth - 1, $expandableModels);
573
            }
574
        }
575
576 20
        return $array;
577
    }
578
}
579