Completed
Push — master ( f81184...3aae6e )
by Andrii
04:04
created

ActiveRecord::attributes()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 18
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 5.0592

Importance

Changes 0
Metric Value
dl 0
loc 18
ccs 13
cts 15
cp 0.8667
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 11
nc 7
nop 0
crap 5.0592
1
<?php
2
/**
3
 * Tools to use API as ActiveRecord for Yii2
4
 *
5
 * @link      https://github.com/hiqdev/yii2-hiart
6
 * @package   yii2-hiart
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2015-2017, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\hiart;
12
13
use yii\base\InvalidConfigException;
14
use yii\base\NotSupportedException;
15
use yii\db\ActiveQueryInterface;
16
use yii\db\BaseActiveRecord;
17
use yii\helpers\ArrayHelper;
18
use yii\helpers\Inflector;
19
use yii\helpers\StringHelper;
20
21
class ActiveRecord extends BaseActiveRecord
22
{
23
    /**
24
     * Returns the database connection used by this AR class.
25
     * By default, the "hiart" application component is used as the database connection.
26
     * You may override this method if you want to use a different database connection.
27
     *
28
     * @return AbstractConnection the database connection used by this AR class
29
     */
30
    public static function getDb()
31
    {
32
        return AbstractConnection::getDb();
33
    }
34
35
    /**
36
     * {@inheritdoc}
37
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
38
     */
39 2
    public static function find()
40
    {
41 2
        $class = static::getDb()->activeQueryClass;
42
43 2
        return new $class(get_called_class());
44
    }
45
46
    /**
47
     * This function is called from `Query::prepare`.
48
     * You can redefine it to get desired behavior.
49
     */
50
    public static function prepare(Query $query, QueryBuilderInterface $builder)
0 ignored issues
show
Unused Code introduced by
The parameter $query 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...
Unused Code introduced by
The parameter $builder 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...
51
    {
52
    }
53
54
    public function isScenarioDefault()
55
    {
56
        return $this->scenario === static::SCENARIO_DEFAULT;
57
    }
58
59
    /**
60
     * Gets a record by its primary key.
61
     *
62
     * @param mixed $primaryKey the primaryKey value
63
     * @param array $options    options given in this parameter are passed to API
64
     *
65
     * @return null|static the record instance or null if it was not found
66
     */
67
    public static function get($primaryKey = null, $options = [])
68
    {
69
        if ($primaryKey === null) {
70
            return null;
71
        }
72
        $command = static::getDb()->createCommand();
73
        $result  = $command->get(static::from(), $primaryKey, $options);
0 ignored issues
show
Documentation Bug introduced by
The method get does not exist on object<hiqdev\hiart\Command>? 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...
74
75
        if ($result) {
76
            $model = static::instantiate($result);
77
            static::populateRecord($model, $result);
78
            $model->afterFind();
79
80
            return $model;
81
        }
82
83
        return null;
84
    }
85
86
    /**
87
     * This method defines the attribute that uniquely identifies a record.
88
     *
89
     * The primaryKey for HiArt objects is the `id` field by default. This field is not part of the
90
     * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
91
     *
92
     * You may override this method to define the primary key name.
93
     *
94
     * Note that HiArt only supports _one_ attribute to be the primary key. However to match the signature
95
     * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
96
     * single string.
97
     *
98
     * @return string[] array of primary key attributes. Only the first element of the array will be used.
99
     */
100
    public static function primaryKey()
101
    {
102
        return ['id'];
103
    }
104
105
    /**
106
     * +     * The name of the main attribute
107
     * +     *
108
     * Examples:.
109
     *
110
     * This will directly reference to the attribute 'name'
111
     * ```
112
     *     return 'name';
113
     * ```
114
     *
115
     * This will concatenate listed attributes, separated with `delimiter` value.
116
     * If delimiter is not set, space is used by default.
117
     * ```
118
     *     return ['seller', 'client', 'delimiter' => '/'];
119
     * ```
120
     *
121
     * The callable method, that will get [[$model]] and should return value of name attribute
122
     * ```
123
     *     return function ($model) {
124
     *        return $model->someField ? $model->name : $model->otherName;
125
     *     };
126
     * ```
127
     *
128
     * @throws InvalidConfigException
129
     *
130
     * @return string|callable|array
131
     *
132
     * @author SilverFire
133
     */
134
    public function primaryValue()
135
    {
136
        return static::formName();
137
    }
138
139
    /**
140
     * Returns the value of the primary attribute.
141
     *
142
     * @throws InvalidConfigException
143
     *
144
     * @return mixed|null
145
     *
146
     * @see primaryValue()
147
     */
148
    public function getPrimaryValue()
149
    {
150
        $primaryValue = $this->primaryValue();
151
152
        if ($primaryValue instanceof \Closure) {
153
            return call_user_func($primaryValue, [$this]);
154
        } elseif (is_array($primaryValue)) {
155
            $delimiter = ArrayHelper::remove($primaryValue, 'delimiter', ' ');
156
157
            return implode($delimiter, $this->getAttributes($primaryValue));
158
        } else {
159
            return $this->getAttribute($primaryValue);
160
        }
161
    }
162
163
    /**
164
     * Returns the list of attribute names.
165
     * By default, this method returns all attributes mentioned in rules.
166
     * You may override this method to change the default behavior.
167
     * @return string[] list of attribute names
168
     */
169 2
    public function attributes()
170
    {
171 2
        $attributes = [];
172 2
        foreach ($this->rules() as $rule) {
173 2
            $names = reset($rule);
174 2
            if (is_string($names)) {
175 2
                $names = [$names];
176 2
            }
177 2
            foreach ($names as $name) {
0 ignored issues
show
Bug introduced by
The expression $names of type object|integer|double|null|array|boolean 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...
178 2
                if (substr_compare($name, '!', 0, 1) === 0) {
179
                    $name = mb_substr($name, 1);
180
                }
181 2
                $attributes[$name] = $name;
182 2
            }
183 2
        }
184
185 2
        return array_values($attributes);
186
    }
187
188
    /**
189
     * @return string the name of the index this record is stored in
190
     */
191
    public static function index()
192
    {
193
        //        return Inflector::pluralize(Inflector::camel2id(StringHelper::basename(get_called_class()), '-'));
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% 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...
194
        return mb_strtolower(StringHelper::basename(get_called_class()) . 's');
195
    }
196
197
    public static function joinIndex()
198
    {
199
        return static::index();
200
    }
201
202
    /**
203
     * Creates an active record instance.
204
     *
205
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
206
     * It is not meant to be used for creating new records directly.
207
     *
208
     * You may override this method if the instance being created
209
     * depends on the row data to be populated into the record.
210
     * For example, by creating a record based on the value of a column,
211
     * you may implement the so-called single-table inheritance mapping.
212
     *
213
     * @return static the newly created active record
214
     */
215 2
    public static function instantiate($row)
216
    {
217 2
        return new static();
218
    }
219
220
    /**
221
     * @return string the name of the entity of this record
222
     */
223
    public static function from()
224
    {
225
        return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
226
    }
227
228
    /**
229
     * Declares the name of the model associated with this class.
230
     * By default this method returns the class name by calling [[Inflector::camel2id()]].
231
     *
232
     * @return string the module name
233
     */
234
    public static function modelName()
235
    {
236
        return Inflector::camel2id(StringHelper::basename(get_called_class()));
237
    }
238
239
    public function insert($runValidation = true, $attributes = null, $options = [])
240
    {
241
        if ($runValidation && !$this->validate($attributes)) {
242
            return false;
243
        }
244
245
        if (!$this->beforeSave(true)) {
246
            return false;
247
        }
248
249
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 239 can also be of type array; however, yii\db\BaseActiveRecord::getDirtyAttributes() does only seem to accept array<integer,string>|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
250
        $data   = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
251
        $result = $this->performScenario('insert', $data);
252
253
        $pk        = static::primaryKey()[0];
254
        $this->$pk = $result['id'];
255
        if ($pk !== 'id') {
256
            $values[$pk] = $result['id'];
257
        }
258
        $changedAttributes = array_fill_keys(array_keys($values), null);
259
        $this->setOldAttributes($values);
260
        $this->afterSave(true, $changedAttributes);
261
262
        return true;
263
    }
264
265
    /**
266
     * {@inheritdoc}
267
     */
268
    public function delete($options = [])
269
    {
270
        if (!$this->beforeDelete()) {
271
            return false;
272
        }
273
274
        $data   = array_merge($options, ['id' => $this->getOldPrimaryKey()]);
275
        $result = $this->performScenario('delete', $data);
276
277
        $this->setOldAttributes(null);
278
        $this->afterDelete();
279
280
        return $result === false ? false : true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result === false ? false : true; (boolean) is incompatible with the return type of the parent method yii\db\BaseActiveRecord::delete of type integer|false.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
281
    }
282
283
    public function update($runValidation = true, $attributeNames = null, $options = [])
284
    {
285
        if ($runValidation && !$this->validate($attributeNames)) {
286
            return false;
287
        }
288
289
        return $this->updateInternal($attributeNames, $options);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->updateInternal($attributeNames, $options); of type integer|boolean adds the type boolean to the return on line 289 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::update of type false|integer.
Loading history...
290
    }
291
292
    protected function updateInternal($attributes = null, $options = [])
293
    {
294
        if (!$this->beforeSave(false)) {
295
            return false;
296
        }
297
298
        $values = $this->getAttributes($attributes);
299
        if (empty($values)) {
300
            $this->afterSave(false, $values);
301
302
            return 0;
303
        }
304
305
        $result = $this->performScenario('update', $values, $options);
306
307
        $changedAttributes = [];
308
        foreach ($values as $name => $value) {
309
            $changedAttributes[$name] = $this->getOldAttribute($name);
310
            $this->setOldAttribute($name, $value);
311
        }
312
313
        $this->afterSave(false, $changedAttributes);
314
315
        return $result === false ? false : true;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $result === false ? false : true; (boolean) is incompatible with the return type of the parent method yii\db\BaseActiveRecord::updateInternal of type false|integer.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
316
    }
317
318
    public function performScenario($defaultScenario, $data, array $options = [])
319
    {
320
        $action = $this->getScenarioAction($defaultScenario);
321
322
        return static::perform($action, $data, $options);
323
    }
324
325
    public static function perform($action, $data, array $options = [])
326
    {
327
        return static::getDb()->createCommand()->perform($action, static::from(), $data, $options);
328
    }
329
330
    /**
331
     * Converts scenario name to action.
332
     * @param string $default default action name
333
     * @throws InvalidConfigException
334
     * @throws NotSupportedException
335
     * @return string
336
     */
337
    public function getScenarioAction($default = '')
338
    {
339
        if ($this->isScenarioDefault()) {
340
            if ($default !== '') {
341
                $result = Inflector::id2camel($default);
342
            } else {
343
                throw new InvalidConfigException('Scenario not specified');
344
            }
345
        } else {
346
            $scenarioCommands = static::scenarioCommands();
347
            if ($action = $scenarioCommands[$this->scenario]) {
348
                if ($action === false) {
349
                    throw new NotSupportedException('The scenario can not be saved');
350
                }
351
352
                if (is_array($action) && $action[0] === null) {
353
                    $result = $action[1];
354
                } elseif (is_array($action)) {
355
                    $result = $action;
356
                } else {
357
                    $result = Inflector::id2camel($action);
358
                }
359
            } else {
360
                $result = Inflector::id2camel($this->scenario);
361
            }
362
        }
363
364
        return is_array($result) ? implode('', $result) : $result;
365
    }
366
367
    /**
368
     * Define an array of relations between scenario and API call action.
369
     *
370
     * Example:
371
     *
372
     * ```
373
     * [
374
     *      'update-name'                => 'set-name', /// ModuleSetName
375
     *      'update-related-name'        => [Action::formName(), 'SetName'], /// ActionSetName
376
     *      'update-self-case-sensetive' => [null, 'SomeSENSETIVE'] /// ModuleSomeSENSETIVE
377
     * ]
378
     * ~~
379
     *
380
     *  key string name of scenario
381
     *  value string|array
382
     *              string will be passed to [[Inflector::id2camel|id2camel]] inflator
383
     *              array - first attribute a module name, second - value
384
     *
385
     * Tricks: pass null as first argument of array to leave command's case unchanged (no inflator calling)
386
     *
387
     * @return array
388
     */
389
    public function scenarioCommands()
390
    {
391
        return [];
392
    }
393
394
    /**
395
     * @return bool
396
     */
397
    public function getIsNewRecord()
398
    {
399
        return !$this->getPrimaryKey();
400
    }
401
402
    /**
403
     * This method has no effect in HiArt ActiveRecord.
404
     */
405
    public function optimisticLock()
406
    {
407
        return null;
408
    }
409
410
    /**
411
     * Destroys the relationship in current model.
412
     *
413
     * This method is not supported by HiArt.
414
     */
415
    public function unlinkAll($name, $delete = false)
416
    {
417
        throw new NotSupportedException('unlinkAll() is not supported by HiArt, use unlink() instead.');
418
    }
419
420
    /**
421
     * {@inheritdoc}
422
     *
423
     * @return ActiveQueryInterface|ActiveQuery the relational query object. If the relation does not exist
424
     *                                          and `$throwException` is false, null will be returned.
425
     */
426
    public function getRelation($name, $throwException = true)
427
    {
428
        return parent::getRelation($name, $throwException);
429
    }
430
431
    /**
432
     * {@inheritdoc}
433
     * @return ActiveQuery the relational query object
434
     */
435
    public function hasOne($class, $link)
436
    {
437
        return parent::hasOne($class, $link);
438
    }
439
440
    /**
441
     * {@inheritdoc}
442
     * @return ActiveQuery the relational query object
443
     */
444
    public function hasMany($class, $link)
445
    {
446
        return parent::hasMany($class, $link);
447
    }
448
}
449