Completed
Push — master ( af8ac7...8742d2 )
by James Ekow Abaka
02:19
created

RecordWrapper::createNew()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 9.4285
cc 1
eloc 4
nc 1
nop 0
crap 1
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\utils\Text;
30
31
/**
32
 * Wraps a record from the database with data manipulation operations.
33
 * Wrapping a table with the record wrapper makes it possible to add, edit,
34
 * delete and query the underlying database. An MVC framework can use the 
35
 * record wrapper as a base for its Model class.
36
 */
37
class RecordWrapper implements \ArrayAccess, \Countable, \Iterator {
38
39
    protected $hasMany = [];
40
    protected $belongsTo = [];
41
    protected $manyHaveMany = [];
42
    protected $behaviours = [];
43
    protected $table;
44
    protected $schema;
45
    private $quotedTable;
46
    private $unquotedTable;
47
    private $modelData = [];
48
    private $invalidFields;
49
    private $dynamicOperations;
50
    private $index = 0;
51
    private $dataSet = false;
52
    
53
    /**
54
     *
55
     * @var DriverAdapter
56
     */
57
    private $adapter;
58
    private $container;
59
    private $context;
60
    private $keys = [];
61
    private $initialized = false;
62
63
    protected function initialize() {
64
        if($this->initialized) return;
65
        $this->context = ORMContext::getInstance($this->container);
66
        $this->container = $this->context->getContainer();
67
        $this->adapter = $this->container->resolve(DriverAdapter::class);
68
        $table = $this->table ?? $this->context->getModelTable($this);
69
        $driver = $this->context->getDbContext()->getDriver();
70
        $this->adapter->setContext($this->context);
71
        if (is_string($table)) {
72
            //$this->quotedTable = $driver->quoteIdentifier($table);
0 ignored issues
show
Unused Code Comprehensibility introduced by
62% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
73
            $this->table = $this->unquotedTable = $table;
74
        } else {
75
            $this->table = $table['table'];
76
            $this->schema = $table['schema'];
77
        }
78
        $this->quotedTable = ($this->schema ? "{$driver->quoteIdentifier($this->schema)}." : "").$driver->quoteIdentifier($this->table);
79
        $this->unquotedTable = ($this->schema ? "{$this->schema}." : "").$this->table;
80
        $this->adapter->setModel($this, $this->quotedTable);
81
        foreach ($this->behaviours as $behaviour) {
82
            $behaviourInstance = $this->getComponentInstance($behaviour);
0 ignored issues
show
Documentation Bug introduced by
The method getComponentInstance 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...
83
            $behaviourInstance->setModel($this);
84
        }
85
        $this->initialized = true;
86
    }
87
    
88
    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...
89
        $data = $this->getData();
90
        return $this->hasMultipleItems() ? $data : isset($data[0]) ? $data[0] : [];
91
    }
92
93
    /**
94
     * 
95
     * @return ModelDescription
96
     */
97
    public function getDescription() {
98
        $this->initialize();
99
        return $this->context->getCache()->read(
100
            (new \ReflectionClass($this))->getName().'::desc', function() {
101
                return $this->container->resolve(ModelDescription::class, ['model' => $this]);
102
            }
103
        );
104
    }
105
106
    /**
107
     * Return the number of items stored in the model or the query.
108
     * @return integer
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...
109
     */
110
    public function count() {
111
        if ($this->dataSet) {
112
            return count($this->getData());
113
        } else {
114
            return $this->__call('count', []);
0 ignored issues
show
Documentation introduced by
'count' is of type string, but the function expects a object<ntentan\nibii\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
array() is of type array, but the function expects a object<ntentan\nibii\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
115
        }
116
    }
117
118
    /**
119
     * Retrieve an item stored in the record.
120
     * This method returns items that are directly stored in the model or lazy
121
     * loads related items. The key could be a field in the model's table or
122
     * the name of a related model.
123
     * @param string $key A key identifying the item to be retrieved.
124
     * @return mixed
125
     */
126
    private function retrieveItem($key) {
127
        $relationships = $this->getDescription()->getRelationships();
128
        $decamelizedKey = Text::deCamelize($key);
129
        if (isset($relationships[$key])) {
130
            return $this->fetchRelatedFields($relationships[$key]);
131
        }
132
        return isset($this->modelData[$decamelizedKey]) ? $this->modelData[$decamelizedKey] : null;
133
    }
134
135
    /**
136
     * @method
137
     * @param type $name
138
     * @param type $arguments
139
     * @return type
140
     */
141
    public function __call($name, $arguments) {
142
        $this->initialize();
143
        if ($this->dynamicOperations === null) {
144
            // Bind to existing instances
145
            $this->container->bind(RecordWrapper::class)->to($this);
146
            $this->dynamicOperations = $this->container->resolve(
147
                Operations::class, ['table' => $this->quotedTable, 'adapter' => $this->adapter]
148
            );
149
            // Unbind all bindings (necessary?)
150
        }
151
        return $this->dynamicOperations->perform($name, $arguments);
152
    }
153
154
    public function __set($name, $value) {
155
        $this->dataSet = true;
156
        $this->modelData[Text::deCamelize($name)] = $value;
157
    }
158
159
    public function __get($name) {
160
        return $this->retrieveItem($name);
161
    }
162
163
    private function expandArrayValue($array, $relationships, $depth, $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...
164
        foreach ($relationships as $name => $relationship) {
165
            $array[$name] = $this->fetchRelatedFields($relationship, $index)->toArray($depth);
166
        }
167
        return $array;
168
    }
169
170
    public function toArray($depth = 0) {
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...
171
        $relationships = $this->getDescription()->getRelationships();
172
        $array = $this->modelData;
173
        if ($depth > 0) {
174
            if ($this->hasMultipleItems()) {
175
                foreach ($array as $i => $value) {
176
                    $array[$i] = $this->expandArrayValue($value, $relationships, $depth - 1, $i);
177
                }
178
            } else {
179
                $array = $this->expandArrayValue($array, $relationships, $depth - 1);
180
            }
181
        }
182
        return $array;
183
    }
184
185
    public function save() {
186
        $return = $this->__call('save', [$this->hasMultipleItems()]);
0 ignored issues
show
Documentation introduced by
'save' is of type string, but the function expects a object<ntentan\nibii\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Documentation introduced by
array($this->hasMultipleItems()) is of type array<integer,boolean,{"0":"boolean"}>, but the function expects a object<ntentan\nibii\type>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
187
        $this->invalidFields = $this->dynamicOperations->getInvalidFields();
188
        return $return;
189
    }
190
191
    private function hasMultipleItems() {
192
        if (count($this->modelData) > 0) {
193
            return is_numeric(array_keys($this->modelData)[0]);
194
        } else {
195
            return false;
196
        }
197
    }
198
199
    public function getData() {
200
        $data = [];
201
202
        if (count($this->modelData) == 0) {
203
            $data = $this->modelData;
204
        } else if ($this->hasMultipleItems()) {
205
            $data = $this->modelData;
206
        } else if (count($this->modelData) > 0) {
207
            $data[] = $this->modelData;
208
        }
209
210
        return $data;
211
    }
212
213
    public function setData($data) {
214
        $this->dataSet = true;
215
        $this->modelData = $data;
216
    }
217
218
    public function mergeData($data) {
219
        foreach ($data as $key => $value) {
220
            $this->modelData[$key] = $value;
221
        }
222
        $this->dataSet = true;
223
    }
224
225
    public function offsetExists($offset) {
226
        return isset($this->modelData[$offset]);
227
    }
228
229
    public function offsetGet($offset) {
230
        if (is_numeric($offset)) {
231
            return $this->wrap($offset);
232
        } else {
233
            return $this->retrieveItem($offset);
234
        }
235
    }
236
237
    public function offsetSet($offset, $value) {
238
        $this->dataSet = true;
239
        $this->modelData[$offset] = $value;
240
    }
241
242
    public function offsetUnset($offset) {
243
        unset($this->modelData[$offset]);
244
    }
245
246
    private function wrap($offset) {
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...
247
        if (isset($this->modelData[$offset])) {
248
            $newInstance = $this->createNew();
0 ignored issues
show
Documentation Bug introduced by
The method createNew 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...
249
            $newInstance->setData($this->modelData[$offset]);
250
            return $newInstance;
251
        } else {
252
            return null;
253
        }
254
    }
255
256
    public function getInvalidFields() {
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...
257
        return $this->invalidFields;
258
    }
259
260
    public function getHasMany() {
261
        return $this->hasMany;
262
    }
263
264
    public function getBelongsTo() {
265
        return $this->belongsTo;
266
    }
267
268
    public function current() {
269
        return $this->wrap($this->keys[$this->index]);
270
    }
271
272
    public function key() {
273
        return $this->keys[$this->index];
274
    }
275
276
    public function next() {
277
        $this->index++;
278
    }
279
280
    public function rewind() {
281
        $this->keys = array_keys($this->modelData);
282
        $this->index = 0;
283
    }
284
285
    public function valid() {
286
        return isset($this->keys[$this->index]) && isset($this->modelData[$this->keys[$this->index]]);
287
    }
288
289
    public function onValidate($errors) {
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...
290
        return $errors;
291
    }
292
293
    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...
294
        if ($index === null) {
295
            $data = $this->modelData;
296
        } else {
297
            $data = $this->modelData[$index];
298
        }
299
        $model = $relationship->getModelInstance();
300
        if (empty($data)) {
301
            return $model;
302
        }
303
        $query = $relationship->prepareQuery($data);
304
        return $query ? $model->fetch($query) : $model;
305
    }
306
307
    public function getRelationships() {
308
        return [
309
            'HasMany' => $this->hasMany,
310
            'BelongsTo' => $this->belongsTo,
311
            'ManyHaveMany' => $this->manyHaveMany
312
        ];
313
    }
314
315
    public function usetField($field) {
316
        unset($this->modelData[$field]);
317
    }
318
319
    public function preSaveCallback() {
320
        
321
    }
322
323
    public function postSaveCallback($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...
324
        
325
    }
326
327
    public function preUpdateCallback() {
328
        
329
    }
330
331
    public function postUpdateCallback() {
332
        
333
    }
334
335
    public function getDBStoreInformation() {
336
        $this->initialize();
337
        return [
338
            'schema' => $this->schema,
339
            'table' => $this->table,
340
            'quoted_table' => $this->quotedTable,
341
            'unquoted_table' => $this->unquotedTable
342
        ];
343
    }
344
    
345
    /**
346
     * 
347
     * @return DataAdapter
0 ignored issues
show
Documentation introduced by
Should the return type not be DriverAdapter?

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...
348
     */
349
    public function getAdapter() {
350
        $this->initialize();
351
        return $this->adapter;
352
    }
353
354
}
355