Completed
Push — master ( 86ece3...f34c1c )
by Nekrasov
02:29
created

BaseBitrixModel::getValueFromLanguageField()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 1
1
<?php
2
3
namespace Arrilot\BitrixModels\Models;
4
5
use Arrilot\BitrixModels\Models\Traits\ModelEventsTrait;
6
use Arrilot\BitrixModels\Queries\BaseQuery;
7
use Illuminate\Support\Collection;
8
use LogicException;
9
10
abstract class BaseBitrixModel extends ArrayableModel
11
{
12
    use ModelEventsTrait;
13
14
    /**
15
     * @var string|null
16
     */
17
    protected static $currentLanguage = null;
18
    
19
    /**
20
     * Array of model fields keys that needs to be saved with next save().
21
     *
22
     * @var array
23
     */
24
    protected $fieldsSelectedForSave = [];
25
26
    /**
27
     * Array of errors that are passed to model events.
28
     *
29
     * @var array
30
     */
31
    protected $eventErrors = [];
32
33
    /**
34
     * Have fields been already fetched from DB?
35
     *
36
     * @var bool
37
     */
38
    protected $fieldsAreFetched = false;
39
40
    /**
41
     * Internal part of create to avoid problems with static and inheritance
42
     *
43
     * @param $fields
44
     *
45
     * @throws LogicException
46
     *
47
     * @return static|bool
48
     */
49
    protected static function internalCreate($fields)
50
    {
51
        throw new LogicException('internalCreate is not implemented');
52
    }
53
54
    /**
55
     * Save model to database.
56
     *
57
     * @param array $selectedFields save only these fields instead of all.
58
     *
59
     * @return bool
60
     */
61
    abstract public function save($selectedFields = []);
62
63
    /**
64
     * Determine whether the field should be stopped from passing to "update".
65
     *
66
     * @param string $field
67
     * @param mixed  $value
68
     * @param array  $selectedFields
69
     *
70
     * @return bool
71
     */
72
    abstract protected function fieldShouldNotBeSaved($field, $value, $selectedFields);
73
    
74
    /**
75
     * Get all model attributes from cache or database.
76
     *
77
     * @return array
78
     */
79
    public function get()
80
    {
81
        $this->load();
82
        
83
        return $this->fields;
84
    }
85
86
    /**
87
     * Load model fields from database if they are not loaded yet.
88
     *
89
     * @return $this
90
     */
91
    public function load()
92
    {
93
        if (!$this->fieldsAreFetched) {
94
            $this->refresh();
95
        }
96
        
97
        return $this;
98
    }
99
100
    /**
101
     * Get model fields from cache or database.
102
     *
103
     * @return array
104
     */
105
    public function getFields()
106
    {
107
        if ($this->fieldsAreFetched) {
108
            return $this->fields;
109
        }
110
        
111
        return $this->refreshFields();
112
    }
113
114
    /**
115
     * Refresh model from database and place data to $this->fields.
116
     *
117
     * @return array
118
     */
119
    public function refresh()
120
    {
121
        return $this->refreshFields();
122
    }
123
124
    /**
125
     * Refresh model fields and save them to a class field.
126
     *
127
     * @return array
128
     */
129
    public function refreshFields()
130
    {
131
        if ($this->id === null) {
132
            $this->original = [];
133
            return $this->fields = [];
134
        }
135
        
136
        $this->fields = static::query()->getById($this->id)->fields;
137
        $this->original = $this->fields;
138
        
139
        $this->fieldsAreFetched = true;
140
        
141
        return $this->fields;
142
    }
143
144
    /**
145
     * Fill model fields if they are already known.
146
     * Saves DB queries.
147
     *
148
     * @param array $fields
149
     *
150
     * @return void
151
     */
152
    public function fill($fields)
153
    {
154
        if (!is_array($fields)) {
155
            return;
156
        }
157
        
158
        if (isset($fields['ID'])) {
159
            $this->id = $fields['ID'];
160
        }
161
        
162
        $this->fields = $fields;
163
        
164
        $this->fieldsAreFetched = true;
165
        
166
        if (method_exists($this, 'afterFill')) {
167
            $this->afterFill();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Arrilot\BitrixModels\Models\BaseBitrixModel as the method afterFill() does only exist in the following sub-classes of Arrilot\BitrixModels\Models\BaseBitrixModel: Arrilot\BitrixModels\Models\ElementModel, Arrilot\BitrixModels\Models\UserModel. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

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

class MyUser extends 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 sub-classes 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 parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
168
        }
169
170
        $this->original = $this->fields;
171
    }
172
173
    /**
174
     * Set current model id.
175
     *
176
     * @param $id
177
     */
178
    protected function setId($id)
179
    {
180
        $this->id = $id;
181
        $this->fields['ID'] = $id;
182
    }
183
184
    /**
185
     * Create new item in database.
186
     *
187
     * @param $fields
188
     *
189
     * @throws LogicException
190
     *
191
     * @return static|bool
192
     */
193
    public static function create($fields)
194
    {
195
        return static::internalCreate($fields);
196
    }
197
198
    /**
199
     * Get count of items that match $filter.
200
     *
201
     * @param array $filter
202
     *
203
     * @return int
204
     */
205
    public static function count(array $filter = [])
206
    {
207
        return static::query()->filter($filter)->count();
208
    }
209
210
    /**
211
     * Get item by its id.
212
     *
213
     * @param int $id
214
     *
215
     * @return static|bool
216
     */
217
    public static function find($id)
218
    {
219
        return static::query()->getById($id);
220
    }
221
222
    /**
223
     * Update model.
224
     *
225
     * @param array $fields
226
     *
227
     * @return bool
228
     */
229
    public function update(array $fields = [])
230
    {
231
        $keys = [];
232
        foreach ($fields as $key => $value) {
233
            array_set($this->fields, $key, $value);
234
            $keys[] = $key;
235
        }
236
237
        return $this->save($keys);
238
    }
239
240
    /**
241
     * Create an array of fields that will be saved to database.
242
     *
243
     * @param $selectedFields
244
     *
245
     * @return array|null
246
     */
247
    protected function normalizeFieldsForSave($selectedFields)
248
    {
249
        $fields = [];
250
        if ($this->fields === null) {
251
            return [];
252
        }
253
254
        foreach ($this->fields as $field => $value) {
255
            if (!$this->fieldShouldNotBeSaved($field, $value, $selectedFields)) {
256
                $fields[$field] = $value;
257
            }
258
        }
259
260
        return $fields ?: null;
261
    }
262
263
    /**
264
     * Instantiate a query object for the model.
265
     *
266
     * @throws LogicException
267
     *
268
     * @return BaseQuery
269
     */
270
    public static function query()
271
    {
272
        throw new LogicException('public static function query() is not implemented');
273
    }
274
275
    /**
276
     * Handle dynamic static method calls into a new query.
277
     *
278
     * @param  string  $method
279
     * @param  array  $parameters
280
     * @return mixed
281
     */
282
    public static function __callStatic($method, $parameters)
283
    {
284
        return static::query()->$method(...$parameters);
285
    }
286
287
    /**
288
     * Returns the value of a model property.
289
     *
290
     * This method will check in the following order and act accordingly:
291
     *
292
     *  - a property defined by a getter: return the getter result
293
     *
294
     * Do not call this method directly as it is a PHP magic method that
295
     * will be implicitly called when executing `$value = $component->property;`.
296
     * @param string $name the property name
297
     * @return mixed the property value
298
     * @throws \Exception if the property is not defined
299
     * @see __set()
300
     */
301
    public function __get($name)
302
    {
303
        // Если уже сохранен такой релейшн, то возьмем его
304
        if (isset($this->related[$name]) || array_key_exists($name, $this->related)) {
305
            return $this->related[$name];
306
        }
307
308
        // Если нет сохраненных данных, ищем подходящий геттер
309
        $getter = $name;
310
        if (method_exists($this, $getter)) {
311
            // read property, e.g. getName()
312
            $value = $this->$getter();
313
314
            // Если геттер вернул запрос, значит $name - релейшен. Нужно выполнить запрос и сохранить во внутренний массив
315
            if ($value instanceof BaseQuery) {
316
                $this->related[$name] = $value->findFor();
317
                return $this->related[$name];
318
            }
319
        }
320
321
        throw new \Exception('Getting unknown property: ' . get_class($this) . '::' . $name);
322
    }
323
324
    /**
325
     * Получить запрос для релейшена по имени
326
     * @param string $name - название релейшена, например `orders` для релейшена, определенного через метод getOrders()
327
     * @param bool $throwException - кидать ли исключение в случае ошибки
328
     * @return BaseQuery - запрос для подгрузки релейшена
329
     * @throws \InvalidArgumentException
330
     */
331
    public function getRelation($name, $throwException = true)
332
    {
333
        $getter = $name;
334
        try {
335
            $relation = $this->$getter();
336
        } catch (\BadMethodCallException $e) {
337
            if ($throwException) {
338
                throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".', 0, $e);
339
            }
340
341
            return null;
342
        }
343
344
        if (!$relation instanceof BaseQuery) {
345
            if ($throwException) {
346
                throw new \InvalidArgumentException(get_class($this) . ' has no relation named "' . $name . '".');
347
            }
348
349
            return null;
350
        }
351
352
        return $relation;
353
    }
354
355
    /**
356
     * Reset event errors back to default.
357
     */
358
    protected function resetEventErrors()
359
    {
360
        $this->eventErrors = [];
361
    }
362
363
    /**
364
     * Declares a `has-one` relation.
365
     * The declaration is returned in terms of a relational [[BaseQuery]] instance
366
     * through which the related record can be queried and retrieved back.
367
     *
368
     * A `has-one` relation means that there is at most one related record matching
369
     * the criteria set by this relation, e.g., a customer has one country.
370
     *
371
     * For example, to declare the `country` relation for `Customer` class, we can write
372
     * the following code in the `Customer` class:
373
     *
374
     * ```php
375
     * public function country()
376
     * {
377
     *     return $this->hasOne(Country::className(), 'ID', 'PROPERTY_COUNTRY');
378
     * }
379
     * ```
380
     *
381
     * Note that in the above, the 'ID' key in the `$link` parameter refers to an attribute name
382
     * in the related class `Country`, while the 'PROPERTY_COUNTRY' value refers to an attribute name
383
     * in the current BaseBitrixModel class.
384
     *
385
     * Call methods declared in [[BaseQuery]] to further customize the relation.
386
     *
387
     * @param string $class the class name of the related record
388
     * @param string $foreignKey
389
     * @param string $localKey
390
     * @return BaseQuery the relational query object.
391
     */
392
    public function hasOne($class, $foreignKey, $localKey = 'ID')
393
    {
394
        return $this->createRelationQuery($class, $foreignKey, $localKey, false);
395
    }
396
397
    /**
398
     * Declares a `has-many` relation.
399
     * The declaration is returned in terms of a relational [[BaseQuery]] instance
400
     * through which the related record can be queried and retrieved back.
401
     *
402
     * A `has-many` relation means that there are multiple related records matching
403
     * the criteria set by this relation, e.g., a customer has many orders.
404
     *
405
     * For example, to declare the `orders` relation for `Customer` class, we can write
406
     * the following code in the `Customer` class:
407
     *
408
     * ```php
409
     * public function orders()
410
     * {
411
     *     return $this->hasMany(Order::className(), 'PROPERTY_COUNTRY_VALUE', 'ID');
412
     * }
413
     * ```
414
     *
415
     * Note that in the above, the 'customer_id' key in the `$link` parameter refers to
416
     * an attribute name in the related class `Order`, while the 'id' value refers to
417
     * an attribute name in the current BaseBitrixModel class.
418
     *
419
     * Call methods declared in [[BaseQuery]] to further customize the relation.
420
     *
421
     * @param string $class the class name of the related record
422
     * @param string $foreignKey
423
     * @param string $localKey
424
     * @return BaseQuery the relational query object.
425
     */
426
    public function hasMany($class, $foreignKey, $localKey = 'ID')
427
    {
428
        return $this->createRelationQuery($class, $foreignKey, $localKey, true);
429
    }
430
431
    /**
432
     * Creates a query instance for `has-one` or `has-many` relation.
433
     * @param string $class the class name of the related record.
434
     * @param string $foreignKey
435
     * @param string $localKey
436
     * @param bool $multiple whether this query represents a relation to more than one record.
437
     * @return BaseQuery the relational query object.
438
     * @see hasOne()
439
     * @see hasMany()
440
     */
441
    protected function createRelationQuery($class, $foreignKey, $localKey, $multiple)
442
    {
443
        /* @var $class BaseBitrixModel */
444
        /* @var $query BaseQuery */
445
        $query = $class::query();
446
        $query->foreignKey = $localKey;
447
        $query->localKey = $foreignKey;
448
        $query->primaryModel = $this;
449
        $query->multiple = $multiple;
450
        return $query;
451
    }
452
453
    /**
454
     * Записать модели как связанные
455
     * @param string $name - название релейшена
456
     * @param Collection|BaseBitrixModel $records - связанные модели
457
     * @see getRelation()
458
     */
459
    public function populateRelation($name, $records)
460
    {
461
        $this->related[$name] = $records;
462
    }
463
    
464
    /**
465
     * Setter for currentLanguage.
466
     *
467
     * @param $language
468
     * @return mixed
469
     */
470
    public static function setCurrentLanguage($language)
471
    {
472
        self::$currentLanguage = $language;
473
    }
474
    
475
    /**
476
     * Getter for currentLanguage.
477
     *
478
     * @return string
479
     */
480
    public static function getCurrentLanguage()
481
    {
482
        return self::$currentLanguage;
483
    }
484
    
485
    /**
486
     * Get value from language field according to current language.
487
     *
488
     * @param $field
489
     * @return mixed
490
     */
491
    protected function getValueFromLanguageField($field)
492
    {
493
        $key = $field . '_' . $this->getCurrentLanguage();
494
495
        return isset($this->fields[$key]) ? $this->fields[$key] : null;
496
    }
497
}
498