GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — master (#45)
by
unknown
05:09
created

MultilingualBehavior   D

Complexity

Total Complexity 90

Size/Duplication

Total Lines 526
Duplicated Lines 8.75 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 33.92%

Importance

Changes 22
Bugs 1 Features 2
Metric Value
wmc 90
c 22
b 1
f 2
lcom 1
cbo 7
dl 46
loc 526
ccs 58
cts 171
cp 0.3392
rs 4.8717

24 Methods

Rating   Name   Duplication   Size   Complexity  
A events() 0 10 1
F attach() 17 100 29
A createLangClass() 0 16 2
A getTranslations() 0 4 1
A getTranslation() 0 6 2
A beforeValidate() 0 6 2
C afterFind() 10 41 14
A afterInsert() 9 9 2
A afterUpdate() 10 10 2
A afterDelete() 0 8 2
C saveTranslations() 0 34 8
A canGetProperty() 0 5 4
A canSetProperty() 0 4 1
A __get() 0 11 3
A __set() 0 11 3
A __isset() 0 8 2
A hasLangAttribute() 0 4 1
A getLangAttribute() 0 4 2
A setLangAttribute() 0 4 1
A indexByLanguage() 0 9 2
A getLanguageBaseName() 0 4 2
A getShortClassName() 0 4 1
A getCurrentLanguage() 0 4 1
A getAttributeName() 0 5 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like MultilingualBehavior 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 MultilingualBehavior, and based on these observations, apply Extract Interface, too.

1
<?php
2
namespace omgdef\multilingual;
3
4
use Yii;
5
use yii\base\Behavior;
6
use yii\base\UnknownPropertyException;
7
use yii\base\InvalidConfigException;
8
use yii\db\ActiveQuery;
9
use yii\db\ActiveRecord;
10
use yii\helpers\Inflector;
11
use yii\validators\Validator;
12
13
class MultilingualBehavior extends Behavior
14
{
15
    /**
16
     * Multilingual attributes
17
     * @var array
18
     */
19
    public $attributes;
20
21
    /**
22
     * Available languages
23
     * It can be a simple array: array('fr', 'en') or an associative array: array('fr' => 'Français', 'en' => 'English')
24
     * For associative arrays, only the keys will be used.
25
     * @var array
26
     */
27
    public $languages;
28
29
    /**
30
     * @var string the default language.
31
     * Example: 'en'.
32
     */
33
    public $defaultLanguage;
34
35
    /**
36
     * @var string the name of the translation table
37
     */
38
    public $tableName;
39
40
    /**
41
     * @var string the name of translation model class.
42
     */
43
    public $langClassName;
44
45
    /**
46
     * @var string the name of the foreign key field of the translation table related to base model table.
47
     */
48
    public $langForeignKey;
49
50
    /**
51
     * @var string the prefix of the localized attributes in the lang table. Here to avoid collisions in queries.
52
     * In the translation table, the columns corresponding to the localized attributes have to be name like this: 'l_[name of the attribute]'
53
     * and the id column (primary key) like this : 'l_id'
54
     * Default to ''.
55
     */
56
    public $localizedPrefix = '';
57
58
    /**
59
     * @var string the name of the lang field of the translation table. Default to 'language'.
60
     */
61
    public $languageField = 'language';
62
63
    /**
64
     * @var boolean if this property is set to true required validators will be applied to all translation models.
65
     * Default to false.
66
     */
67
    public $requireTranslations = false;
68
69
    /**
70
     * @var boolean whether to force deletion of the associated translations when a base model is deleted.
71
     * Not needed if using foreign key with 'on delete cascade'.
72
     * Default to true.
73
     */
74
    public $forceDelete = true;
75
76
    /**
77
     * @var boolean whether to dynamically create translation model class.
78
     * If true, the translation model class will be generated on runtime with the use of the eval() function so no additional php file is needed.
79
     * See {@link createLangClass()}
80
     * Default to true.
81
     */
82
    public $dynamicLangClass = true;
83
84
    /**
85
     * @var boolean whether to abridge the language ID.
86
     * Default to true.
87
     */
88
    public $abridge = true;
89
90
    /**
91
     * @var string the name of the primary key field of the base model. Defaults to first value of Model::primaryKey.
92
     */
93
    public $ownerPrimaryKey;
94
95
    /**
96
     * @var boolean whether to check for existing translations on insert
97
     * Default to false
98
     */
99
    public $checkOnInsert = false;
100
101
    private $currentLanguage;
102
    private $ownerClassName;
103
    private $langClassShortName;
104
    private $ownerClassShortName;
105
    private $langAttributes = [];
106
107
    /**
108
     * @var array excluded validators
109
     */
110
    private $excludedValidators = ['unique'];
111
112
    /**
113
     * @inheritdoc
114
     */
115 9
    public function events()
116
    {
117
        return [
118 9
            ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
119 9
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
120 9
            ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
121 9
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
122 9
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
123
        ];
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129 9
    public function attach($owner)
130
    {
131
        /** @var ActiveRecord $owner */
132
        parent::attach($owner);
133
134 View Code Duplication
        if (empty($this->languages) || !is_array($this->languages)) {
135
            throw new InvalidConfigException('Please specify array of available languages for the ' . get_class($this) . ' in the '
136
                . get_class($this->owner) . ' or in the application parameters', 101);
137
        }
138
139
        if (array_values($this->languages) !== $this->languages) { //associative array
140
            $this->languages = array_keys($this->languages);
141
        }
142
143
        $this->languages = array_unique(array_map(function ($language) {
144
            return $this->getLanguageBaseName($language);
145
        }, $this->languages));
146
147 9
        if (!$this->defaultLanguage) {
148
            $this->defaultLanguage = isset(Yii::$app->params['defaultLanguage']) && Yii::$app->params['defaultLanguage'] ?
149
                Yii::$app->params['defaultLanguage'] : Yii::$app->language;
150
        }
151
152
        $this->defaultLanguage = $this->getLanguageBaseName($this->defaultLanguage);
153
154 9
        if (!$this->currentLanguage) {
155
            $this->currentLanguage = $this->getLanguageBaseName(Yii::$app->language);
156
        }
157
158 View Code Duplication
        if (empty($this->attributes) || !is_array($this->attributes)) {
159
            throw new InvalidConfigException('Please specify multilingual attributes for the ' . get_class($this) . ' in the '
160
                . get_class($this->owner), 103);
161
        }
162
163 9
        if (!$this->langClassName) {
164
            $this->langClassName = get_class($this->owner) . 'Lang';
165
        }
166
167
        $this->langClassShortName = $this->getShortClassName($this->langClassName);
168
        $this->ownerClassName = get_class($this->owner);
169
        $this->ownerClassShortName = $this->getShortClassName($this->ownerClassName);
170
171
        /** @var ActiveRecord $className */
172 9
        $className = $this->ownerClassName;
173 9
        if (!isset($this->ownerPrimaryKey)) {
174
            $this->ownerPrimaryKey = $className::primaryKey()[0];
175
        }
176
177 9
        if (!isset($this->langForeignKey)) {
178
            throw new InvalidConfigException('Please specify langForeignKey for the ' . get_class($this) . ' in the '
179
                . get_class($this->owner), 105);
180
        }
181
182
        $rules = $owner->rules();
183
        $validators = $owner->getValidators();
184
185
        foreach ($rules as $rule) {
186
            if (in_array($rule[1], $this->excludedValidators))
187
                continue;
188
189
            $rule_attributes = is_array($rule[0]) ? $rule[0] : [$rule[0]];
190
            $attributes = array_intersect($this->attributes, $rule_attributes);
191
192
            if (empty($attributes))
193
                continue;
194
195
            $rule_attributes = [];
196
            foreach ($attributes as $key => $attribute) {
197
                foreach ($this->languages as $language)
198
                    if ($language != $this->defaultLanguage)
199
                        $rule_attributes[] = $this->getAttributeName($attribute, $language);
200
            }
201
202
            if (isset($rule['skipOnEmpty']) && !$rule['skipOnEmpty'])
203
                $rule['skipOnEmpty'] = !$this->requireTranslations;
204
205
            $params = array_slice($rule, 2);
206
207
            if ($rule[1] !== 'required' || $this->requireTranslations) {
208
                $validators[] = Validator::createValidator($rule[1], $owner, $rule_attributes, $params);
209 8
            } elseif ($rule[1] === 'required') {
210
                $validators[] = Validator::createValidator('safe', $owner, $rule_attributes, $params);
211 8
            }
212
        }
213
214 9
        if ($this->dynamicLangClass) {
215
            $this->createLangClass();
216
        }
217
218
        $translation = new $this->langClassName;
219 9 View Code Duplication
        foreach ($this->languages as $lang) {
220
            foreach ($this->attributes as $attribute) {
221
                $attributeName = $this->localizedPrefix . $attribute;
222
                $this->setLangAttribute($this->getAttributeName($attribute, $lang), $translation->{$attributeName});
223
                if ($lang == $this->defaultLanguage) {
224
                    $this->setLangAttribute($attribute, $translation->{$attributeName});
225
                }
226
            }
227
        }
228
    }
229
230 9
    public function createLangClass()
231
    {
232
        if (!class_exists($this->langClassName, false)) {
233
            $namespace = substr($this->langClassName, 0, strrpos($this->langClassName, '\\'));
234
            eval('
235
            namespace ' . $namespace . ';
236
            use yii\db\ActiveRecord;
237
            class ' . $this->langClassShortName . ' extends ActiveRecord
238
            {
239
                public static function tableName()
240
                {
241
                    return \'' . $this->tableName . '\';
242
                }
243
            }');
244
        }
245 9
    }
246
247
    /**
248
     * Relation to model translations
249
     * @return ActiveQuery
250
     */
251
    public function getTranslations()
252
    {
253
        return $this->owner->hasMany($this->langClassName, [$this->langForeignKey => $this->ownerPrimaryKey]);
254
    }
255
256
    /**
257
     * Relation to model translation
258
     * @param $language
259
     * @return ActiveQuery
260
     */
261 5
    public function getTranslation($language = null)
262
    {
263
        $language = $language ?: $this->getCurrentLanguage();
264 5
        return $this->owner->hasOne($this->langClassName, [$this->langForeignKey => $this->ownerPrimaryKey])
265
            ->where([$this->languageField => $language]);
266 5
    }
267
268
    /**
269
     * Handle 'beforeValidate' event of the owner.
270
     */
271 10
    public function beforeValidate()
272
    {
273 10
        foreach ($this->attributes as $attribute) {
274
            $this->setLangAttribute($this->getAttributeName($attribute, $this->defaultLanguage), $this->getLangAttribute($attribute));
275
        }
276 10
    }
277
278
    /**
279
     * Handle 'afterFind' event of the owner.
280
     */
281 8
    public function afterFind()
282
    {
283
        /** @var ActiveRecord $owner */
284 8
        $owner = $this->owner;
285
286
        if ($owner->isRelationPopulated('translations') && $related = $owner->getRelatedRecords()['translations']) {
287
            $translations = $this->indexByLanguage($related);
288 4
            foreach ($this->languages as $lang) {
289
                foreach ($this->attributes as $attribute) {
290 View Code Duplication
                    foreach ($translations as $translation) {
291
                        if ($this->getLanguageBaseName($translation->{$this->languageField}) == $lang) {
292
                            $attributeName = $this->localizedPrefix . $attribute;
293
                            $this->setLangAttribute($this->getAttributeName($attribute, $lang), $translation->{$attributeName});
294
295
                            if ($lang == $this->defaultLanguage) {
296
                                $this->setLangAttribute($attribute, $translation->{$attributeName});
297
                            }
298
                        }
299
                    }
300
                }
301
            }
302
        } else {
303
            if (!$owner->isRelationPopulated('translation')) {
304
                $owner->translation;
305
            }
306
307
            $translation = $owner->getRelatedRecords()['translation'];
308 5
            if ($translation) {
309 4
                foreach ($this->attributes as $attribute) {
310
                    $attribute_name = $this->localizedPrefix . $attribute;
311
                    $owner->setLangAttribute($attribute, $translation->$attribute_name);
312
                }
313
            }
314 4
        }
315
316 8
        foreach ($this->attributes as $attribute) {
317
            if ($owner->hasAttribute($attribute) && $this->getLangAttribute($attribute)) {
318
                $owner->setAttribute($attribute, $this->getLangAttribute($attribute));
319
            }
320
        }
321 8
    }
322
323
    /**
324
     * Handle 'afterInsert' event of the owner.
325
     */
326 4 View Code Duplication
    public function afterInsert()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
327
    {
328 4
        $translations = [];
329 4
        if ($this->checkOnInsert)
330
        {
331
            $translations = $this->indexByLanguage($this->owner->getRelatedRecords()['translations']);
332
        }
333
        $this->saveTranslations($translations);
334 4
    }
335
336
    /**
337
     * Handle 'afterUpdate' event of the owner.
338
     */
339 5 View Code Duplication
    public function afterUpdate()
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
340
    {
341
        /** @var ActiveRecord $owner */
342 5
        $owner = $this->owner;
343
344
        if ($owner->isRelationPopulated('translations')) {
345
            $translations = $this->indexByLanguage($owner->getRelatedRecords()['translations']);
346
            $this->saveTranslations($translations);
347
        }
348 5
    }
349
350
    /**
351
     * Handle 'afterDelete' event of the owner.
352
     */
353 2
    public function afterDelete()
354
    {
355 2
        if ($this->forceDelete) {
356
            /** @var ActiveRecord $owner */
357 2
            $owner = $this->owner;
358
            $owner->unlinkAll('translations', true);
359
        }
360 2
    }
361
362
    /**
363
     * @param array $translations
364
     */
365 7
    private function saveTranslations($translations = [])
366
    {
367
        /** @var ActiveRecord $owner */
368 7
        $owner = $this->owner;
369
370 7
        foreach ($this->languages as $lang) {
371
            $defaultLanguage = $lang == $this->defaultLanguage;
372
373
            if (!isset($translations[$lang])) {
374
                /** @var ActiveRecord $translation */
375
                $translation = new $this->langClassName;
376
                $translation->{$this->languageField} = $lang;
377
                $translation->{$this->langForeignKey} = $owner->getAttribute($this->ownerPrimaryKey);
378
            } else {
379
                $translation = $translations[$lang];
380
            }
381
382
            $save = false;
383
            foreach ($this->attributes as $attribute) {
384
                $value = $defaultLanguage ? $owner->$attribute : $this->getLangAttribute($this->getAttributeName($attribute, $lang));
385
386
                if ($value !== null) {
387
                    $field = $this->localizedPrefix . $attribute;
388
                    $translation->$field = $value;
389
                    $save = true;
390
                }
391
            }
392
393
            if ($translation->isNewRecord && !$save)
394 3
                continue;
395
396
            $translation->save();
397
        }
398 7
    }
399
400
    /**
401
     * @inheritdoc
402
     */
403 3
    public function canGetProperty($name, $checkVars = true)
404
    {
405
        return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)
406 3
        || $this->hasLangAttribute($name);
407 3
    }
408
409
    /**
410
     * @inheritdoc
411
     */
412
    public function canSetProperty($name, $checkVars = true)
413
    {
414
        return $this->hasLangAttribute($name);
415
    }
416
417
    /**
418
     * @inheritdoc
419
     */
420
    public function __get($name)
421
    {
422
        try {
423
            return parent::__get($name);
424
        } catch (UnknownPropertyException $e) {
425
            if ($this->hasLangAttribute($name)) return $this->getLangAttribute($name);
426
            // @codeCoverageIgnoreStart
427
            else throw $e;
428
            // @codeCoverageIgnoreEnd
429
        }
430
    }
431
432
    /**
433
     * @inheritdoc
434
     */
435
    public function __set($name, $value)
436
    {
437
        try {
438
            parent::__set($name, $value);
439
        } catch (UnknownPropertyException $e) {
440
            if ($this->hasLangAttribute($name)) $this->setLangAttribute($name, $value);
441
            // @codeCoverageIgnoreStart
442
            else throw $e;
443
            // @codeCoverageIgnoreEnd
444
        }
445
    }
446
447
    /**
448
     * @inheritdoc
449
     * @codeCoverageIgnore
450
     */
451
    public function __isset($name)
452
    {
453
        if (!parent::__isset($name)) {
454
            return $this->hasLangAttribute($name);
455
        } else {
456
            return true;
457
        }
458
    }
459
460
    /**
461
     * Whether an attribute exists
462
     * @param string $name the name of the attribute
463
     * @return boolean
464
     */
465
    public function hasLangAttribute($name)
466
    {
467
        return array_key_exists($name, $this->langAttributes);
468
    }
469
470
    /**
471
     * @param string $name the name of the attribute
472
     * @return string the attribute value
473
     */
474
    public function getLangAttribute($name)
475
    {
476
        return $this->hasLangAttribute($name) ? $this->langAttributes[$name] : null;
477
    }
478
479
    /**
480
     * @param string $name the name of the attribute
481
     * @param string $value the value of the attribute
482
     */
483
    public function setLangAttribute($name, $value)
484
    {
485
        $this->langAttributes[$name] = $value;
486
    }
487
488
    /**
489
     * @param $records
490
     * @return array
491
     */
492 1
    protected function indexByLanguage($records)
493
    {
494 1
        $sorted = array();
495
        foreach ($records as $record) {
496
            $sorted[$record->{$this->languageField}] = $record;
497
        }
498 1
        unset($records);
499 1
        return $sorted;
500
    }
501
502
    /**
503
     * @param $language
504
     * @return string
505
     */
506
    protected function getLanguageBaseName($language)
507
    {
508
        return $this->abridge ? substr($language, 0, 2) : $language;
509
    }
510
511
    /**
512
     * @param string $className
513
     * @return string
514
     */
515
    private function getShortClassName($className)
516
    {
517
        return substr($className, strrpos($className, '\\') + 1);
518
    }
519
520
    /**
521
     * @return mixed|string
522
     */
523 5
    public function getCurrentLanguage()
524
    {
525 5
        return $this->currentLanguage;
526
    }
527
528
    /**
529
     * @param $attribute
530
     * @param $language
531
     * @return string
532
     */
533
    protected function getAttributeName($attribute, $language)
534
    {
535
        $language = $this->abridge ? $language : Inflector::camel2id(Inflector::id2camel($language), "_");
536
        return $attribute . "_" . $language;
537
    }
538
}
539