Completed
Push — master ( 59b3e2...6dd6b9 )
by Andrii
15:38
created

ActiveRecord::getScenarioAction()   D

Complexity

Conditions 9
Paths 12

Size

Total Lines 29
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 90

Importance

Changes 0
Metric Value
dl 0
loc 29
rs 4.909
c 0
b 0
f 0
ccs 0
cts 8
cp 0
cc 9
eloc 20
nc 12
nop 1
crap 90
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 hiqdev\hiart\AbstractConnection;
14
use yii\base\InvalidConfigException;
15
use yii\base\NotSupportedException;
16
use yii\db\ActiveQueryInterface;
17
use yii\db\BaseActiveRecord;
18
use yii\helpers\ArrayHelper;
19
use yii\helpers\Inflector;
20
use yii\helpers\StringHelper;
21
22
class ActiveRecord extends BaseActiveRecord
23
{
24
    /**
25
     * Returns the database connection used by this AR class.
26
     * By default, the "hiart" application component is used as the database connection.
27
     * You may override this method if you want to use a different database connection.
28
     *
29
     * @return Connection the database connection used by this AR class
30
     */
31
    public static function getDb()
32
    {
33
        return AbstractConnection::getDb();
34
    }
35
36
    /**
37
     * {@inheritdoc}
38
     * @return ActiveQuery the newly created [[ActiveQuery]] instance
39
     */
40
    public static function find()
41
    {
42
        $class = static::getDb()->activeQueryClass;
0 ignored issues
show
Bug introduced by
Accessing activeQueryClass on the interface hiqdev\hiart\ConnectionInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
43
44
        return new $class(get_called_class());
45
    }
46
47
    /**
48
     * This function is called from `Query::prepare`.
49
     * You can redefine it to get desired behavior.
50
     */
51
    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...
52
    {
53
    }
54
55
    public function isScenarioDefault()
56
    {
57
        return $this->scenario === static::SCENARIO_DEFAULT;
58
    }
59
60
    /**
61
     * Gets a record by its primary key.
62
     *
63
     * @param mixed $primaryKey the primaryKey value
64
     * @param array $options    options given in this parameter are passed to API
65
     *
66
     * @return null|static the record instance or null if it was not found
67
     */
68
    public static function get($primaryKey = null, $options = [])
69
    {
70
        if ($primaryKey === null) {
71
            return null;
72
        }
73
        $command = static::getDb()->createCommand();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface hiqdev\hiart\ConnectionInterface as the method createCommand() does only exist in the following implementations of said interface: hiqdev\hiart\AbstractConnection, hiqdev\hiart\rest\Connection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
74
        $result  = $command->get(static::from(), $primaryKey, $options);
75
76
        if ($result) {
77
            $model = static::instantiate($result);
78
            static::populateRecord($model, $result);
79
            $model->afterFind();
80
81
            return $model;
82
        }
83
84
        return null;
85
    }
86
87
    /**
88
     * This method defines the attribute that uniquely identifies a record.
89
     *
90
     * The primaryKey for HiArt objects is the `id` field by default. This field is not part of the
91
     * ActiveRecord attributes so you should never add `_id` to the list of [[attributes()|attributes]].
92
     *
93
     * You may override this method to define the primary key name.
94
     *
95
     * Note that HiArt only supports _one_ attribute to be the primary key. However to match the signature
96
     * of the [[\yii\db\ActiveRecordInterface|ActiveRecordInterface]] this methods returns an array instead of a
97
     * single string.
98
     *
99
     * @return string[] array of primary key attributes. Only the first element of the array will be used.
100
     */
101
    public static function primaryKey()
102
    {
103
        return ['id'];
104
    }
105
106
    /**
107
     * +     * The name of the main attribute
108
     * +     *
109
     * Examples:.
110
     *
111
     * This will directly reference to the attribute 'name'
112
     * ```
113
     *     return 'name';
114
     * ```
115
     *
116
     * This will concatenate listed attributes, separated with `delimiter` value.
117
     * If delimiter is not set, space is used by default.
118
     * ```
119
     *     return ['seller', 'client', 'delimiter' => '/'];
120
     * ```
121
     *
122
     * The callable method, that will get [[$model]] and should return value of name attribute
123
     * ```
124
     *     return function ($model) {
125
     *        return $model->someField ? $model->name : $model->otherName;
126
     *     };
127
     * ```
128
     *
129
     * @throws InvalidConfigException
130
     *
131
     * @return string|callable|array
132
     *
133
     * @author SilverFire
134
     */
135
    public function primaryValue()
136
    {
137
        return static::formName();
138
    }
139
140
    /**
141
     * Returns the value of the primary attribute.
142
     *
143
     * @throws InvalidConfigException
144
     *
145
     * @return mixed|null
146
     *
147
     * @see primaryValue()
148
     */
149
    public function getPrimaryValue()
150
    {
151
        $primaryValue = $this->primaryValue();
152
153
        if ($primaryValue instanceof \Closure) {
154
            return call_user_func($primaryValue, [$this]);
155
        } elseif (is_array($primaryValue)) {
156
            $delimiter = ArrayHelper::remove($primaryValue, 'delimiter', ' ');
157
158
            return implode($delimiter, $this->getAttributes($primaryValue));
159
        } else {
160
            return $this->getAttribute($primaryValue);
161
        }
162
    }
163
164
    /**
165
     * Returns the list of attribute names.
166
     * By default, this method returns all attributes mentioned in rules.
167
     * You may override this method to change the default behavior.
168
     * @return string[] list of attribute names.
169
     */
170
    public function attributes()
171
    {
172
        $attributes = [];
173
        foreach ($this->rules() as $rule) {
174
            $names = reset($rule);
175
            if (is_string($names)) {
176
                $names = [$names];
177
            }
178
            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...
179
                if (substr_compare($name, '!', 0, 1) === 0) {
180
                    $name = mb_substr($name, 1);
181
                }
182
                $attributes[$name] = $name;
183
            }
184
        }
185
186
        return array_values($attributes);
187
    }
188
189
    /**
190
     * @return string the name of the index this record is stored in
191
     */
192
    public static function index()
193
    {
194
        //        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...
195
        return mb_strtolower(StringHelper::basename(get_called_class()) . 's');
196
    }
197
198
    public static function joinIndex()
199
    {
200
        return static::index();
201
    }
202
203
    /**
204
     * Creates an active record instance.
205
     *
206
     * This method is called together with [[populateRecord()]] by [[ActiveQuery]].
207
     * It is not meant to be used for creating new records directly.
208
     *
209
     * You may override this method if the instance being created
210
     * depends on the row data to be populated into the record.
211
     * For example, by creating a record based on the value of a column,
212
     * you may implement the so-called single-table inheritance mapping.
213
     *
214
     * @return static the newly created active record
215
     */
216
    public static function instantiate($row)
217
    {
218
        return new static();
219
    }
220
221
    /**
222
     * @return string the name of the entity of this record
223
     */
224
    public static function from()
225
    {
226
        return Inflector::camel2id(StringHelper::basename(get_called_class()), '-');
227
    }
228
229
    /**
230
     * Declares the name of the model associated with this class.
231
     * By default this method returns the class name by calling [[Inflector::camel2id()]].
232
     *
233
     * @return string the module name
234
     */
235
    public static function modelName()
236
    {
237
        return Inflector::camel2id(StringHelper::basename(get_called_class()));
238
    }
239
240
    public function insert($runValidation = true, $attributes = null, $options = [])
241
    {
242
        if ($runValidation && !$this->validate($attributes)) {
243
            return false;
244
        }
245
246
        if (!$this->beforeSave(true)) {
247
            return false;
248
        }
249
250
        $values = $this->getDirtyAttributes($attributes);
0 ignored issues
show
Bug introduced by
It seems like $attributes defined by parameter $attributes on line 240 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...
251
        $data   = array_merge($values, $options, ['id' => $this->getOldPrimaryKey()]);
252
        $result = $this->performScenario('insert', $data);
253
254
        $pk        = static::primaryKey()[0];
255
        $this->$pk = $result['id'];
256
        if ($pk !== 'id') {
257
            $values[$pk] = $result['id'];
258
        }
259
        $changedAttributes = array_fill_keys(array_keys($values), null);
260
        $this->setOldAttributes($values);
261
        $this->afterSave(true, $changedAttributes);
262
263
        return true;
264
    }
265
266
    /**
267
     * {@inheritdoc}
268
     */
269
    public function delete($options = [])
270
    {
271
        if (!$this->beforeDelete()) {
272
            return false;
273
        }
274
275
        $data   = array_merge($options, ['id' => $this->getOldPrimaryKey()]);
276
        $result = $this->performScenario('delete', $data);
277
278
        $this->setOldAttributes(null);
279
        $this->afterDelete();
280
281
        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...
282
    }
283
284
    public function update($runValidation = true, $attributeNames = null, $options = [])
285
    {
286
        if ($runValidation && !$this->validate($attributeNames)) {
287
            return false;
288
        }
289
290
        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 290 which is incompatible with the return type of the parent method yii\db\BaseActiveRecord::update of type false|integer.
Loading history...
291
    }
292
293
    protected function updateInternal($attributes = null, $options = [])
294
    {
295
        if (!$this->beforeSave(false)) {
296
            return false;
297
        }
298
299
        $values = $this->getAttributes($attributes);
300
        if (empty($values)) {
301
            $this->afterSave(false, $values);
302
303
            return 0;
304
        }
305
306
        $result = $this->performScenario('update', $values, $options);
307
308
        $changedAttributes = [];
309
        foreach ($values as $name => $value) {
310
            $changedAttributes[$name] = $this->getOldAttribute($name);
311
            $this->setOldAttribute($name, $value);
312
        }
313
314
        $this->afterSave(false, $changedAttributes);
315
316
        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...
317
    }
318
319
    public function performScenario($defaultScenario, $data, array $options = [])
320
    {
321
        $action = $this->getScenarioAction($defaultScenario);
322
323
        return static::perform($action, $data, $options);
324
    }
325
326
    public static function perform($action, $data, array $options = [])
327
    {
328
        return static::getDb()->createCommand()->perform($action, static::from(), $data, $options);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface hiqdev\hiart\ConnectionInterface as the method createCommand() does only exist in the following implementations of said interface: hiqdev\hiart\AbstractConnection, hiqdev\hiart\rest\Connection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

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