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
03:00
created

MultilingualBehavior::__get()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 3

Importance

Changes 2
Bugs 0 Features 0
Metric Value
c 2
b 0
f 0
dl 0
loc 11
ccs 4
cts 4
cp 1
rs 9.4285
cc 3
eloc 6
nc 3
nop 1
crap 3
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 $loadTranslationsOnInsert = 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 19
    public function events()
116
    {
117
        return [
118 19
            ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
119 19
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
120 19
            ActiveRecord::EVENT_AFTER_INSERT => 'afterInsert',
121 19
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
122 19
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
123 19
        ];
124
    }
125
126
    /**
127
     * @inheritdoc
128
     */
129 19
    public function attach($owner)
130
    {
131
        /** @var ActiveRecord $owner */
132 19
        parent::attach($owner);
133
134 19 View Code Duplication
        if (empty($this->languages) || !is_array($this->languages)) {
135 1
            throw new InvalidConfigException('Please specify array of available languages for the ' . get_class($this) . ' in the '
136 1
                . get_class($this->owner) . ' or in the application parameters', 101);
137
        }
138
139 19
        if (array_values($this->languages) !== $this->languages) { //associative array
140 19
            $this->languages = array_keys($this->languages);
141 19
        }
142
143 19
        $this->languages = array_unique(array_map(function ($language) {
144 19
            return $this->getLanguageBaseName($language);
145 19
        }, $this->languages));
146
147 19
        if (!$this->defaultLanguage) {
148 1
            $this->defaultLanguage = isset(Yii::$app->params['defaultLanguage']) && Yii::$app->params['defaultLanguage'] ?
149 1
                Yii::$app->params['defaultLanguage'] : Yii::$app->language;
150 1
        }
151
152 19
        $this->defaultLanguage = $this->getLanguageBaseName($this->defaultLanguage);
153
154 19
        if (!$this->currentLanguage) {
155 19
            $this->currentLanguage = $this->getLanguageBaseName(Yii::$app->language);
156 19
        }
157
158 19 View Code Duplication
        if (empty($this->attributes) || !is_array($this->attributes)) {
159 1
            throw new InvalidConfigException('Please specify multilingual attributes for the ' . get_class($this) . ' in the '
160 1
                . get_class($this->owner), 103);
161
        }
162
163 19
        if (!$this->langClassName) {
164 19
            $this->langClassName = get_class($this->owner) . 'Lang';
165 19
        }
166
167 19
        $this->langClassShortName = $this->getShortClassName($this->langClassName);
168 19
        $this->ownerClassName = get_class($this->owner);
169 19
        $this->ownerClassShortName = $this->getShortClassName($this->ownerClassName);
170
171
        /** @var ActiveRecord $className */
172 19
        $className = $this->ownerClassName;
173 19
        if (!isset($this->ownerPrimaryKey)) {
174 19
            $this->ownerPrimaryKey = $className::primaryKey()[0];
175 19
        }
176
177 19
        if (!isset($this->langForeignKey)) {
178 1
            throw new InvalidConfigException('Please specify langForeignKey for the ' . get_class($this) . ' in the '
179 1
                . get_class($this->owner), 105);
180
        }
181
182 19
        $rules = $owner->rules();
183 19
        $validators = $owner->getValidators();
184
185 19
        foreach ($rules as $rule) {
186 19
            if (in_array($rule[1], $this->excludedValidators))
187 19
                continue;
188
189 19
            $rule_attributes = is_array($rule[0]) ? $rule[0] : [$rule[0]];
190 19
            $attributes = array_intersect($this->attributes, $rule_attributes);
191
192 19
            if (empty($attributes))
193 19
                continue;
194
195 19
            $rule_attributes = [];
196 19
            foreach ($attributes as $key => $attribute) {
197 19
                foreach ($this->languages as $language)
198 19
                    if ($language != $this->defaultLanguage)
199 19
                        $rule_attributes[] = $this->getAttributeName($attribute, $language);
200 19
            }
201
202 19
            if (isset($rule['skipOnEmpty']) && !$rule['skipOnEmpty'])
203 19
                $rule['skipOnEmpty'] = !$this->requireTranslations;
204
205 19
            $params = array_slice($rule, 2);
206
207 19
            if ($rule[1] !== 'required' || $this->requireTranslations) {
208 19
                $validators[] = Validator::createValidator($rule[1], $owner, $rule_attributes, $params);
209 19
            } elseif ($rule[1] === 'required') {
210 18
                $validators[] = Validator::createValidator('safe', $owner, $rule_attributes, $params);
211 18
            }
212 19
        }
213
214 19
        if ($this->dynamicLangClass) {
215 19
            $this->createLangClass();
216 19
        }
217
218 19
        $translation = new $this->langClassName;
219 19 View Code Duplication
        foreach ($this->languages as $lang) {
220 19
            foreach ($this->attributes as $attribute) {
221 19
                $attributeName = $this->localizedPrefix . $attribute;
222 19
                $this->setLangAttribute($this->getAttributeName($attribute, $lang), $translation->{$attributeName});
223 19
                if ($lang == $this->defaultLanguage) {
224 19
                    $this->setLangAttribute($attribute, $translation->{$attributeName});
225 19
                }
226 19
            }
227 19
        }
228 19
    }
229
230 19
    public function createLangClass()
231
    {
232 19
        if (!class_exists($this->langClassName, false)) {
233 3
            $namespace = substr($this->langClassName, 0, strrpos($this->langClassName, '\\'));
234
            eval('
235 3
            namespace ' . $namespace . ';
236
            use yii\db\ActiveRecord;
237 3
            class ' . $this->langClassShortName . ' extends ActiveRecord
238
            {
239
                public static function tableName()
240
                {
241 3
                    return \'' . $this->tableName . '\';
242
                }
243 3
            }');
244 3
        }
245 19
    }
246
247
    /**
248
     * Relation to model translations
249
     * @return ActiveQuery
250
     */
251 8
    public function getTranslations()
252
    {
253 8
        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 8
    public function getTranslation($language = null)
262
    {
263 8
        $language = $language ?: $this->getCurrentLanguage();
264 8
        return $this->owner->hasOne($this->langClassName, [$this->langForeignKey => $this->ownerPrimaryKey])
265 8
            ->where([$this->languageField => $language]);
266
    }
267
268
    /**
269
     * Handle 'beforeValidate' event of the owner.
270
     */
271 11
    public function beforeValidate()
272
    {
273 11
        foreach ($this->attributes as $attribute) {
274 11
            $this->setLangAttribute($this->getAttributeName($attribute, $this->defaultLanguage), $this->getLangAttribute($attribute));
275 11
        }
276 11
    }
277
278
    /**
279
     * Handle 'afterFind' event of the owner.
280
     */
281 13
    public function afterFind()
282
    {
283
        /** @var ActiveRecord $owner */
284 13
        $owner = $this->owner;
285
286 13
        if ($owner->isRelationPopulated('translations') && $related = $owner->getRelatedRecords()['translations']) {
287 6
            $translations = $this->indexByLanguage($related);
288 6
            foreach ($this->languages as $lang) {
289 6
                foreach ($this->attributes as $attribute) {
290 6 View Code Duplication
                    foreach ($translations as $translation) {
291 6
                        if ($this->getLanguageBaseName($translation->{$this->languageField}) == $lang) {
292 6
                            $attributeName = $this->localizedPrefix . $attribute;
293 6
                            $this->setLangAttribute($this->getAttributeName($attribute, $lang), $translation->{$attributeName});
294
295 6
                            if ($lang == $this->defaultLanguage) {
296 6
                                $this->setLangAttribute($attribute, $translation->{$attributeName});
297 6
                            }
298 6
                        }
299 6
                    }
300 6
                }
301 6
            }
302 6
        } else {
303 8
            if (!$owner->isRelationPopulated('translation')) {
304 6
                $owner->translation;
305 6
            }
306
307 8
            $translation = $owner->getRelatedRecords()['translation'];
308 8
            if ($translation) {
309 7
                foreach ($this->attributes as $attribute) {
310 7
                    $attribute_name = $this->localizedPrefix . $attribute;
311 7
                    $owner->setLangAttribute($attribute, $translation->$attribute_name);
312 7
                }
313 7
            }
314
        }
315
316 13
        foreach ($this->attributes as $attribute) {
317 13
            if ($owner->hasAttribute($attribute) && $this->getLangAttribute($attribute)) {
318 1
                $owner->setAttribute($attribute, $this->getLangAttribute($attribute));
319 1
            }
320 13
        }
321 13
    }
322
323
    /**
324
     * Handle 'afterInsert' event of the owner.
325
     */
326 5
    public function afterInsert()
327
    {
328 5
        $translations = [];
329 5
        if ($this->loadTranslationsOnInsert)
330 5
        {
331
            $translations = $this->indexByLanguage($this->getTranslations()->all());
332
        }
333 5
        $this->saveTranslations($translations);
334 5
    }
335
336
    /**
337
     * Handle 'afterUpdate' event of the owner.
338
     */
339 5
    public function afterUpdate()
340
    {
341
        /** @var ActiveRecord $owner */
342 5
        $owner = $this->owner;
343
344 5
        if ($owner->isRelationPopulated('translations')) {
345 3
            $translations = $this->indexByLanguage($owner->getRelatedRecords()['translations']);
346 3
            $this->saveTranslations($translations);
347 3
        }
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 2
            $owner->unlinkAll('translations', true);
359 2
        }
360 2
    }
361
362
    /**
363
     * @param array $translations
364
     */
365 8
    private function saveTranslations($translations = [])
366
    {
367
        /** @var ActiveRecord $owner */
368 8
        $owner = $this->owner;
369
370 8
        foreach ($this->languages as $lang) {
371 8
            $defaultLanguage = $lang == $this->defaultLanguage;
372
373 8
            if (!isset($translations[$lang])) {
374
                /** @var ActiveRecord $translation */
375 5
                $translation = new $this->langClassName;
376 5
                $translation->{$this->languageField} = $lang;
377 5
                $translation->{$this->langForeignKey} = $owner->getAttribute($this->ownerPrimaryKey);
378 5
            } else {
379 3
                $translation = $translations[$lang];
380
            }
381
382 8
            $save = false;
383 8
            foreach ($this->attributes as $attribute) {
384 8
                $value = $defaultLanguage ? $owner->$attribute : $this->getLangAttribute($this->getAttributeName($attribute, $lang));
385
386 8
                if ($value !== null) {
387 8
                    $field = $this->localizedPrefix . $attribute;
388 8
                    $translation->$field = $value;
389 8
                    $save = true;
390 8
                }
391 8
            }
392
393 8
            if ($translation->isNewRecord && !$save)
394 8
                continue;
395
396 8
            $translation->save();
397 8
        }
398 8
    }
399
400
    /**
401
     * @inheritdoc
402
     */
403 18
    public function canGetProperty($name, $checkVars = true)
404
    {
405 18
        return method_exists($this, 'get' . $name) || $checkVars && property_exists($this, $name)
406 18
        || $this->hasLangAttribute($name);
407
    }
408
409
    /**
410
     * @inheritdoc
411
     */
412 10
    public function canSetProperty($name, $checkVars = true)
413
    {
414 10
        return $this->hasLangAttribute($name);
415
    }
416
417
    /**
418
     * @inheritdoc
419
     */
420 17
    public function __get($name)
421
    {
422
        try {
423 17
            return parent::__get($name);
424 15
        } catch (UnknownPropertyException $e) {
425 15
            if ($this->hasLangAttribute($name)) return $this->getLangAttribute($name);
426
            // @codeCoverageIgnoreStart
427
            else throw $e;
428
            // @codeCoverageIgnoreEnd
429
        }
430
    }
431
432
    /**
433
     * @inheritdoc
434
     */
435 10
    public function __set($name, $value)
436
    {
437
        try {
438 10
            parent::__set($name, $value);
439 10
        } catch (UnknownPropertyException $e) {
440 10
            if ($this->hasLangAttribute($name)) $this->setLangAttribute($name, $value);
441
            // @codeCoverageIgnoreStart
442
            else throw $e;
443
            // @codeCoverageIgnoreEnd
444
        }
445 10
    }
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 15
    public function hasLangAttribute($name)
466
    {
467 15
        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 15
    public function getLangAttribute($name)
475
    {
476 15
        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 19
    public function setLangAttribute($name, $value)
484
    {
485 19
        $this->langAttributes[$name] = $value;
486 19
    }
487
488
    /**
489
     * @param $records
490
     * @return array
491
     */
492 6
    protected function indexByLanguage($records)
493
    {
494 6
        $sorted = array();
495 6
        foreach ($records as $record) {
496 6
            $sorted[$record->{$this->languageField}] = $record;
497 6
        }
498 6
        unset($records);
499 6
        return $sorted;
500
    }
501
502
    /**
503
     * @param $language
504
     * @return string
505
     */
506 19
    protected function getLanguageBaseName($language)
507
    {
508 19
        return $this->abridge ? substr($language, 0, 2) : $language;
509
    }
510
511
    /**
512
     * @param string $className
513
     * @return string
514
     */
515 19
    private function getShortClassName($className)
516
    {
517 19
        return substr($className, strrpos($className, '\\') + 1);
518
    }
519
520
    /**
521
     * @return mixed|string
522
     */
523 8
    public function getCurrentLanguage()
524
    {
525 8
        return $this->currentLanguage;
526
    }
527
528
    /**
529
     * @param $attribute
530
     * @param $language
531
     * @return string
532
     */
533 19
    protected function getAttributeName($attribute, $language)
534
    {
535 19
        $language = $this->abridge ? $language : Inflector::camel2id(Inflector::id2camel($language), "_");
536 19
        return $attribute . "_" . $language;
537
    }
538
}
539